Исправление ПЛОХОГО дизайна базы данных после того, как данные будут в системе

StackOverflow https://stackoverflow.com/questions/76967

  •  09-06-2019
  •  | 
  •  

Вопрос

Я знаю, что это не вопрос...эм, в любом случае, вот в чем вопрос.

Я унаследовал базу данных, в которой есть 1 (одна) таблица, которая выглядит примерно так.Его цель - зафиксировать, какие виды встречаются в различных (200 с лишним) странах.

ID 
Species
Afghanistan
Albania
Algeria
American Samoa
Andorra
Angola
....
Western Sahara
Yemen
Zambia
Zimbabwe

Выборка данных была бы примерно такой

id Species Afghanistan Albania American Samoa
1  SP1         null     null        null
2  SP2          1         1         null
3  SP3         null      null         1

Мне кажется, это типичная ситуация "многие ко многим", и я хочу 3 таблицы.Вид, Страна и виды, найденные в стране

Таблица ссылок (SpeciesFoundInCountry) будет иметь внешние ключи как в таблицах видов, так и в таблицах стран.

(Трудно нарисовать диаграмму!)

Species
SpeciesID  SpeciesName

Country
CountryID CountryName

SpeciesFoundInCountry
CountryID SpeciesID

Есть ли волшебный способ, которым я могу сгенерировать инструкцию insert, которая получит CountryId из новой таблицы Country на основе имени столбца и SpeciesID, где в исходной мега-таблице есть 1?

Я могу сделать это для одной страны (это выбор, чтобы показать, что я хочу получить)

SELECT Species.ID, Country.CountryID
FROM Country, Species
WHERE (((Species.Afghanistan)=1)) AND (((Country.Country)="Afghanistan"));

(мега-таблица называется species)

Но, используя эту стратегию, мне нужно было бы выполнить запрос для каждого столбца в исходной таблице.

Есть ли способ сделать это в sql?

Я думаю, я могу собрать все мои предложения where вместе и написать скрипт для создания sql, хотя это кажется неэлегантным!

Есть какие-нибудь соображения (или требуются пояснения)?

Это было полезно?

Решение

Я бы использовал скрипт для генерации всех отдельных запросов, поскольку это одноразовый процесс импорта.

Некоторые программы, такие как Excel, хороши в смешивании различных измерений данных (сравнении имен столбцов с данными внутри строк), но реляционные базы данных редко это делают.

Однако вы можете обнаружить, что некоторые системы (например, Microsoft Access, что удивительно) имеют удобные инструменты, которые вы можете использовать для нормализации данных.Лично я бы предпочел написать сценарий быстрее, но ваши относительные навыки работы с доступом и написанием сценариев могут отличаться от моих.

Другие советы

Почему вы хотите сделать это в SQL?Просто напишите небольшой скрипт, который выполняет преобразование.

Когда я сталкиваюсь с ними, я пишу скрипт для выполнения преобразования, а не пытаюсь сделать это в SQL.Обычно для меня это происходит намного быстрее и проще.Выберите любой язык, который вам удобен.

Если бы это был SQL Server, вы бы использовали команды Unpivot, но, глядя на присвоенный вами тег, это для access - я прав?

Хотя есть команда поворота в access, обратного утверждения нет.

Похоже, это можно сделать с помощью сложного соединения.Проверьте это интересная статья для получения дополнительной информации о том, как отключить блокировку в команде select.

Вероятно, вы захотите создать таблицы замены на месте.Тип сценария зависит от доступного вам языка сценариев, но вы должны иметь возможность создать таблицу идентификаторов стран, просто перечислив столбцы таблицы, которая у вас есть сейчас.Как только вы это сделаете, вы можете выполнить некоторые замены строк, чтобы просмотреть все уникальные названия стран и вставить в таблицу speciesFoundInCountry, где указанный столбец country не равен null.

Вероятно, вы могли бы поумнеть и запросить в системных таблицах имена столбцов, а затем создать динамическую строку запроса для выполнения, но, честно говоря, это, вероятно, будет более уродливым, чем быстрый скрипт для генерации инструкций SQL для вас.

Надеюсь, у вас не слишком много динамического SQL-кода, который обращается к старым таблицам, скрытым в вашей кодовой базе.Это могло бы быть в самом деле трудная часть.

В SQL Server это сгенерирует ваш пользовательский выбор, который вы демонстрируете.Вы можете экстраполировать на вставку

select 
  'SELECT Species.ID, Country.CountryID FROM Country, Species WHERE (((Species.' + 
 c.name + 
 ')=1)) AND (((Country.Country)="' +
 c.name + 
 '"))'
from syscolumns c
inner join sysobjects o
on o.id = c.id
where o.name = 'old_table_name'

Как и в случае с другими, я бы, скорее всего, просто сделал это как одноразовое быстрое решение любым удобным для вас способом.

При таких типах преобразований это одноразовые элементы, быстрые исправления, и код не обязательно должен быть элегантным, он просто должен работать.Для подобных вещей я делал это многими способами.

Если это SQL Server, вы можете использовать таблицу sys.columns, чтобы найти все столбцы исходной таблицы.Затем вы можете использовать динамический SQL и команду pivot, чтобы делать то, что вы хотите.Посмотрите их синтаксис в Интернете.

Я бы определенно согласился с вашим предложением написать небольшой скрипт для создания вашего SQL-кода с запросом для каждого столбца.

Фактически, ваш скрипт мог бы быть уже закончен за то время, которое вы потратили на обдумывание этого волшебного запроса (который вы использовали бы только один раз, а затем выбросили, так какой смысл делать все это волшебным и совершенным)

Извините, но чертов анализатор публикации удалил пробелы и форматирование в моем посте.Это затрудняет чтение журнала.

@топать:

Над полем, в котором вы вводите ответ, есть несколько кнопок.Тот, который равен 101010, является образцом кода.Вы выделяете весь свой текст, который является кодом, и затем нажимаете эту кнопку.Тогда с этим особо не возятся.

cout>>"I don't know C"
cout>>"Hello World"

Я бы использовал запрос объединения, очень грубо:

Dim db As Database
Dim tdf As TableDef

Set db = CurrentDb

Set tdf = db.TableDefs("SO")

strSQL = "SELECT ID, Species, """ & tdf.Fields(2).Name _
    & """ AS Country, [" & tdf.Fields(2).Name & "] AS CountryValue FROM SO "

For i = 3 To tdf.Fields.Count - 1
    strSQL = strSQL & vbCrLf & "UNION SELECT ID, Species, """ & tdf.Fields(i).Name _
    & """ AS Country, [" & tdf.Fields(i).Name & "] AS CountryValue FROM SO "
Next

db.CreateQueryDef "UnionSO", strSQL

Тогда у вас будет представление, которое можно будет добавить к вашему новому дизайну.

Когда я прочитал название "плохой дизайн базы данных", мне было любопытно узнать, насколько он плох.Ты меня не разочаровал :)

Как упоминали другие, сценарий был бы самым простым способом.Этого можно достичь, написав около 15 строк кода на PHP.

SELECT * FROM ugly_table;
while(row)
foreach(row as field => value)
if(value == 1)
SELECT country_id from country_table WHERE country_name = field;

if(field == 'Species')
SELECT species_id from species_table WHERE species_name = value;

INSERT INTO better_table (...)

Очевидно, что это псевдокод, и он не будет работать таким, какой он есть.Вы также можете заполнить таблицу стран и видов "на лету", добавив сюда инструкции insert.

Извините, я очень мало занимался программированием Access, но я могу предложить некоторые рекомендации, которые должны помочь.

Сначала давайте пройдемся по проблеме.Предполагается, что обычно вам потребуется сгенерировать несколько строк в SpeciesFoundInCountry для каждой строки в исходной таблице.Другими словами, виды, как правило, обитают более чем в одной стране.На самом деле это легко сделать с помощью декартова произведения, соединения без критериев объединения.

Чтобы выполнить декартово произведение, вам нужно будет создать таблицу Country.Таблица должна содержать country_id от 1 до N (N - количество уникальных стран, 200 или около того) и название страны.Чтобы упростить себе жизнь, просто используйте цифры от 1 до N в порядке столбцов.Это сделало бы Афганистан 1 - м , а Албанию 2 - м...Зимбабве Н .Вы должны быть в состоянии использовать системные таблицы для этого.

Затем создайте таблицу или представление из исходной таблицы, которая содержит виды и жало с 0 или 1 для каждой страны.Вам нужно будет преобразовать значение null, а не null в текст 0 или 1 и объединить все значения в одну строку.Описание таблицы и текстовый редактор с регулярными выражениями должны упростить это.Поэкспериментируйте сначала с одним столбцом, и как только это сработает, отредактируйте режим создания представления / вставки со всеми столбцами.

Затем соедините две таблицы вместе без каких-либо критериев объединения.Это даст вам рекорд по каждому виду в каждой стране, вы почти на месте.

Теперь все, что вам нужно сделать, это отфильтровать недопустимые записи, они будут иметь ноль в соответствующем месте строки.Поскольку столбец country_code таблицы country имеет местоположение подстроки, все, что вам нужно сделать, это отфильтровать записи, где оно равно 0.

where substring(new_column,country_code) = '1'

Вам все равно нужно будет создать таблицу видов и присоединиться к ней

where a.species_name = b.species_name

a и b - это псевдонимы таблиц.

Надеюсь, это поможет

OBTW,

Если у вас есть запросы, которые уже выполняются к старой таблице, вам нужно будет создать представление, которое реплицирует старые таблицы, используя новые таблицы.Вам нужно будет выполнить группировку по, чтобы денормализовать таблицы.

Сообщите своим пользователям, что старая таблица / представление не будет поддерживаться в будущем, и все новые запросы или обновления к старым запросам должны будут использовать новые таблицы.

Если мне когда-нибудь придется создать кучу похожих SQL-инструкций и выполнить их все, я часто нахожу, что Excel очень удобен.Возьмите свой первоначальный запрос.Если у вас есть список стран в столбце A и ваш оператор SQL в столбце B, оформленный в виде текста (в кавычках) со ссылками на ячейки, вставленными там, где страна отображается в sql

например ,="ВСТАВИТЬ В новую таблицу SELECT ...(вид." & A1 & ")= ...));"

затем просто скопируйте формулу вниз, чтобы создать 200 различных операторов SQL, скопируйте / вставьте столбец в свой редактор и нажмите F5.Конечно, вы можете сделать это с любым количеством переменных, сколько захотите.

Когда я сталкивался с подобными проблемами, я счел удобным сгенерировать скрипт, который генерирует SQL-скрипты.Вот образец, который вы привели, абстрагированный для использования %PAR1% вместо Афганистана.

SELECT Species.ID, Country.CountryID
FROM Country, Species
WHERE (((Species.%PAR1%)=1)) AND (((Country.Country)="%PAR1%"))
UNION

Также было добавлено ключевое слово union, чтобы объединить все выбранные элементы.

Далее вам понадобится список стран, сгенерированный на основе ваших существующих данных:

Афганистан Албания ., .

Далее вам нужен скрипт, который может выполнять итерации по списку стран, и для каждой итерации выдавать результат, который заменяет Афганистан на %PAR1% на первой итерации, Албанию на второй итерации и так далее.Алгоритм точно такой же, как слияние почты в текстовом редакторе.Это небольшая работа по написанию этого сценария.Но, получив его, вы сможете использовать в десятках разовых проектов, подобных этому.

Наконец, вам нужно вручную изменить последнее "ОБЪЕДИНЕНИЕ" обратно на точку с запятой.

Если вы сможете получить доступ для выполнения этого гигантского объединения, вы сможете получить нужные вам данные в нужной форме и вставить их в свою новую таблицу.

Я бы сделал это трехэтапным процессом с небольшими временными изменениями в вашей таблице SpeciesFoundInCountry.Я бы добавил столбец в эту таблицу для хранения названия страны.Тогда шаги были бы следующими.

1) Создайте / запустите скрипт, который обходит столбцы в исходной таблице и создает запись в SpeciesFoundInCountry для каждого столбца, имеющего истинное значение.Эта запись будет содержать название страны.2) Запустите инструкцию SQL, которая обновит SpeciesFoundInCountry.Поле CountryId путем присоединения к таблице Country по названию страны.3) Очистите таблицу SpeciesFoundInCountry, удалив столбец countryName.

Вот небольшой псевдокод MS Access VB / VBA, который даст вам представление о сути

Public Sub CreateRelationshipRecords()

  Dim rstSource as DAO.Recordset
  Dim rstDestination as DAO.Recordset
  Dim fld as DAO.Field
  dim strSQL as String
  Dim lngSpeciesID as Long

  strSQL = "SELECT * FROM [ORIGINALTABLE]"
  Set rstSource = CurrentDB.OpenRecordset(strSQL)
  set rstDestination = CurrentDB.OpenRecordset("SpeciesFoundInCountry")

  rstSource.MoveFirst

  ' Step through each record in the original table
  Do Until rstSource.EOF
    lngSpeciesID = rstSource.ID
    ' Now step through the fields(columns). If the field
    ' value is one (1), then create a relationship record
    ' using the field name as the Country Name
    For Each fld in rstSource.Fields
      If fld.Value = 1 then
        with rstDestination
          .AddNew
          .Fields("CountryID").Value = Null
          .Fields("CountryName").Value = fld.Name
          .Fields("SpeciesID").Value = lngSpeciesID
          .Update
        End With
      End IF
    Next fld  
    rstSource.MoveNext
  Loop

  ' Clean up
  rstSource.Close
  Set rstSource = nothing
  ....

End Sub

После этого вы могли бы запустить простую инструкцию SQL для обновления значений CountryId в таблице SpeciesFoundInCountry.

ОБНОВИТЕ ВНУТРЕННЕЕ ОБЪЕДИНЕНИЕ SpeciesFoundInCountry ДЛЯ SpeciesFoundInCountry.countryName = Страна.ЗАДАЙТЕ название страны SpeciesFoundInCountry.countryName = Страна.Идентификатор страны;

Наконец, все, что вам нужно сделать, это очистить таблицу SpeciesFoundInCountry, удалив столбец countryName.

**** ПРИМЕЧАНИЕ СБОКУ:Я счел полезным иметь таблицы стран, которые также включают сокращения ISO (коды стран).Иногда они используются в качестве внешних ключей в других таблицах, так что соединение с таблицей стран не обязательно включать в запросы.

Для получения дополнительной информации: http://en.wikipedia.org/wiki/Iso_country_codes

Это (надеюсь) одноразовое упражнение, поэтому неэлегантное решение может быть не таким плохим, как кажется.

Проблема (о чем, я уверен, вы прекрасно осведомлены!) заключается в том, что в какой-то момент вашего запроса вы должны перечислить все эти столбцы.: (Вопрос в том, какой самый элегантный способ сделать это?Ниже приведена моя попытка.Это выглядит громоздко, потому что в нем так много столбцов, но, возможно, это то, что вам нужно, или, по крайней мере, это может указать вам правильное направление.

Возможное SQL-решение:

/* if you have N countries */
CREATE TABLE Country
(id    int, 
 name  varchar(50)) 

INSERT Country
      SELECT 1, 'Afghanistan'
UNION SELECT 2, 'Albania', 
UNION SELECT 3, 'Algeria' ,
UNION SELECT 4, 'American Samoa' ,
UNION SELECT 5, 'Andorra' ,
UNION SELECT 6, 'Angola' ,
...
UNION SELECT N-3, 'Western Sahara', 
UNION SELECT N-2, 'Yemen', 
UNION SELECT N-1, 'Zambia', 
UNION SELECT N, 'Zimbabwe', 



CREATE TABLE #tmp
(key        varchar(N),  
 country_id int) 
/* "key" field needs to be as long as N */  


INSERT #tmp 
SELECT '1________ ... _', 'Afghanistan' 
/* '1' followed by underscores to make the length = N */

UNION SELECT '_1_______ ... ___', 'Albania'
UNION SELECT '__1______ ... ___', 'Algeria'
...
UNION SELECT '________ ... _1_', 'Zambia'
UNION SELECT '________ ... __1', 'Zimbabwe'

CREATE TABLE new_table
(country_id int, 
species_id int) 

INSERT new_table
SELECT species.id, country_id
FROM   species s , 
       #tmp    t
WHERE  isnull( s.Afghanistan, ' ' ) +  
       isnull( s.Albania, ' ' ) +  
       ... +  
       isnull( s.Zambia, ' ' ) +  
       isnull( s.Zimbabwe, ' ' ) like t.key 

Мое Предложение

Лично я бы не стал этого делать.Я бы сделал быстрое и грязное решение, подобное тому, на которое вы ссылаетесь, за исключением того, что я бы жестко запрограммировал идентификаторы стран (потому что вы собираетесь сделать это только один раз, не так ли?И вы можете сделать это сразу после создания таблицы country, чтобы знать, каковы все идентификаторы):

INSERT new_table SELECT Species.ID, 1 FROM Species WHERE Species.Afghanistan = 1 
INSERT new_table SELECT Species.ID, 2 FROM Species WHERE Species.Albania= 1 
...
INSERT new_table SELECT Species.ID, 999 FROM Species WHERE Species.Zambia= 1 
INSERT new_table SELECT Species.ID, 1000 FROM Species WHERE Species.Zimbabwe= 1 
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top