Управление версиями в таблицах SQL - как с этим справиться? - PullRequest
34 голосов
/ 22 сентября 2010

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

TABLE EMPLOYEE: (with personal commentary)

|ID | VERSION | NAME       | Position | PAY |
+---+---------+------------+----------+-----+
| 1 |    1    | John Doe   | Owner    | 100 | Started company
| 1 |    2    | John Doe   | Owner    |  80 | Pay cut to hire a coder
| 2 |    1    | Mark May   | Coder    |  20 | Hire said coder
| 2 |    2    | Mark May   | Coder    |  30 | Productive coder gets raise
| 3 |    1    | Jane Field | Admn Asst|  15 | Need office staff
| 2 |    3    | Mark May   | Coder    |  35 | Productive coder gets raise
| 1 |    3    | John Doe   | Owner    | 120 | Sales = profit for owner!
| 3 |    2    | Jane Field | Admn Asst|  20 | Raise for office staff
| 4 |    1    | Cody Munn  | Coder    |  20 | Hire another coder
| 4 |    2    | Cody Munn  | Coder    |  25 | Give that coder raise
| 3 |    3    | Jane Munn  | Admn Asst|  20 | Jane marries Cody <3
| 2 |    4    | Mark May   | Dev Lead |  40 | Promote mark to Dev Lead
| 4 |    3    | Cody Munn  | Coder    |  30 | Give Cody a raise
| 2 |    5    | Mark May   | Retired  |   0 | Mark retires
| 5 |    1    | Joey Trib  | Dev Lead |  40 | Bring outside help for Dev Lead
| 6 |    1    | Hire Meplz | Coder    |  10 | Hire a cheap coder
| 3 |    4    | Jane Munn  | Retired  |   0 | Jane quits
| 7 |    1    | Work Fofre | Admn Asst|  10 | Hire Janes replacement
| 8 |    1    | Fran Hesky | Coder    |  10 | Hire another coder
| 9 |    1    | Deby Olav  | Coder    |  25 | Hire another coder
| 4 |    4    | Cody Munn  | VP Ops   |  80 | Promote Cody
| 9 |    2    | Deby Olav  | VP Ops   |  80 | Cody fails at VP Ops, promote Deby
| 4 |    5    | Cody Munn  | Retired  |   0 | Cody retires in shame
| 5 |    2    | Joey Trib  | Dev Lead |  50 | Give Joey a raise
+---+---------+------------+----------+-----+

Теперь, если бы я хотел сделать что-то вроде «Получить список текущих кодировщиков», я бы не смог просто сделать SELECT * FROM EMPLOYEE WHERE Position = 'Coder' потому что это возвратило бы много исторических данных ... что плохо.

Я ищу хорошие идеи, чтобы справиться с этим сценарием.Я вижу несколько вариантов, которые бросаются в глаза, но я уверен, что кто-то скажет: «Вау, это ошибка новичка, сияй ... примерь это для размера:», что это за место, о чем все, верно?: -)

Идея № 1: Сохраните таблицу версий с текущей версией, подобной этой

TABLE EMPLOYEE_VERSION:

|ID |VERSION|
+---+-------+
| 1 |   3   |
| 2 |   5   |
| 3 |   4   |
| 4 |   6   |
| 5 |   2   |
| 6 |   1   |
| 7 |   1   |
| 8 |   1   |
| 9 |   2   |     
+---+-------+

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

Конечно, мне придется обновлять эту таблицу каждый раз, когда я вставляюв таблицу EMPLOYEE для увеличения версии для данного идентификатора (или вставки в таблицу версий при создании нового идентификатора).

Излишние издержки кажутся нежелательными.

Идеяномер 2: Храните архивную таблицу и основную таблицу.Перед обновлением основной таблицы вставьте строку, которую я собираюсь перезаписать, в таблицу архива и используйте основную таблицу, как обычно, как если бы меня не беспокоило управление версиями.

Идея № 3: Найдите запрос, который добавляет что-то вроде SELECT * FROM EMPLOYEE WHERE Position = 'Coder' and version=MaxVersionForId(EMPLOYEE.ID) ... Не совсем уверен, как бы я это сделал.Мне кажется, это лучшая идея, но я действительно не уверен в этом.

Идея № 4: Создайте столбец для «current» и добавьте «WHERE current = true AND».... "

Мне приходит в голову, что наверняка люди уже делали это раньше, сталкивались с такими же проблемами, и у них было понимание, чтобы поделиться ими, и поэтому я пришел, чтобы собрать это!:) Я уже пытался найти примеры проблем здесь, но они кажутся специализированными для определенного сценария.

Спасибо!

РЕДАКТИРОВАТЬ 1:

Во-первых, я ценю все ответы, и вы все сказали одно и то же - DATE лучше, чем VERSION NUMBER.Одна из причин, по которой я шел с VERSION NUMBER, заключалась в том, чтобы упростить процесс обновления на сервере, чтобы предотвратить следующий сценарий

Человек A загружает запись сотрудника 3 в своем сеансе и имеет версию 4. Человек B загружает сотрудниказапись 3 в своем сеансе, и он имеет версию 4. Человек А вносит изменения и фиксирует.Это работает, потому что самой последней версией в базе данных является 4. Теперь это 5. Человек B вносит изменения и фиксирует.Это терпит неудачу, потому что самая последняя версия - 5, а его - 4.

Как шаблон EFFECTIVE DATE решит эту проблему?

EDIT 2:

Я думаю, что я мог бы сделать это, выполнив что-то вроде этого: Человек А загружает запись 3 сотрудника в его сеансе, и его дата вступления в силу 1-1-2010, 1:00 вечера, без каких-либо излишеств.Человек B загружает запись 3 сотрудника в его сеансе, и его дата вступления в силу 1-1-2010, 1:00 вечера, без каких-либо излишествЧеловек А вносит изменения и совершает.Старая копия отправляется в архивную таблицу (в основном идея 2) с датой завершения работы 22.09.2010 13:00.Обновленная версия основной таблицы имеет дату вступления в силу 22.09.2010 13:00.Человек Б вносит изменения и совершает.Не удается выполнить фиксацию, поскольку даты вступления в силу (в базе данных и сеансе) не совпадают.

Ответы [ 7 ]

33 голосов
/ 22 сентября 2010

Я думаю, вы пошли по неверному пути.

Как правило, для управления версиями или хранения исторических данных вы делаете одно из двух (или оба).

  1. У вас есть отдельная таблица, которая имитирует исходную таблицу + столбец даты / времени для даты, когда она была изменена. Всякий раз, когда запись обновляется, вы вставляете существующее содержимое в таблицу истории непосредственно перед обновлением.

  2. У вас есть отдельная база данных склада. В этом случае вы можете либо сделать версию так же, как в # 1 выше, либо просто сделать снимок один раз очень часто (ежечасно, ежедневно, еженедельно ..)

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

Во-вторых, радикально увеличится сложность вашего запроса для объединений и т. Д., Чтобы обеспечить использование последней версии каждой записи.

28 голосов
/ 12 января 2012

То, что у вас здесь, называется медленно меняющимся измерением (SCD).Есть несколько проверенных методов борьбы с ним:

http://en.wikipedia.org/wiki/Slowly_changing_dimension

Я бы добавил, что никто не называет его по имени.

10 голосов
/ 05 апреля 2011

Подход, который я разработал для недавней базы данных, заключается в использовании ревизий следующим образом:

  • Храните свою сущность в двух таблицах:

    1. «сотрудник» хранит идентификатор первичного ключа и любые данные, которые вы не хотите, чтобы версионные версии (если они есть).

    2. "employee_revision" хранит все существенные данные о сотруднике с внешним ключом для таблицы сотрудника и внешним ключом "RevisionID" для таблицы с именем "revision".

  • Создайте новую таблицу с именем "revision". Это может использоваться всеми объектами в вашей базе данных, а не только сотрудником. Он содержит столбец идентификации для первичного ключа (или AutoNumber, или как бы то ни было в вашей базе данных). Он также содержит столбцы EffectiveFrom и EffectiveTo. У меня также есть текстовый столбец в таблице - entity_type - для удобства чтения, который содержит имя основной таблицы изменений (в данном случае «сотрудник»). Таблица ревизий не содержит внешних ключей. Значение по умолчанию для EffectiveFrom - 1 января 1900 года, а значение по умолчанию для EffectiveTo - 31 декабря 9999 года. Это позволяет мне не упрощать запрос даты.

Я уверен, что таблица ревизий хорошо проиндексирована (EffectiveFrom, EffectiveTo, RevisionID), а также (RevisionID, EffectiveFrom, EffectiveTo).

Затем я могу использовать соединения и простые <> сравнения, чтобы выбрать подходящую запись для любой даты. Это также означает, что отношения между сущностями также полностью версионированы. На самом деле, я считаю полезным использовать табличные функции SQL Server, чтобы очень просто запрашивать любую дату.

Вот пример (при условии, что вы не хотите создавать версии имен сотрудников, чтобы, если они меняют свое имя, изменение вступает в силу исторически).

--------
employee
--------
employee_id  |  employee_name
-----------  |  -------------
12351        |  John Smith

-----------------
employee_revision
-----------------
employee_id  |  revision_id  |  department_id  |  position_id  |  pay
-----------  |  -----------  |  -------------  |  -----------  |  ----------
12351        |  657442       |  72             |  23           |  22000.00
12351        |  657512       |  72             |  27           |  22000.00
12351        |  657983       |  72             |  27           |  28000.00

--------
revision
--------
revision_id  |  effective_from  |  effective_to  |  entity_type
-----------  |  --------------  |  ------------  |  -----------
657442       |  01-Jan-1900     |  03-Mar-2007   |  EMPLOYEE
657512       |  04-Mar-2007     |  22-Jun-2009   |  EMPLOYEE
657983       |  23-Jun-2009     |  31-Dec-9999   |  EMPLOYEE

Одним из преимуществ хранения метаданных редакции в отдельной таблице является то, что ее легко применять последовательно ко всем вашим сущностям. Другое заключается в том, что его проще расширить, включив в него другие вещи, такие как ветви или сценарии, без необходимости изменять каждую таблицу. Моя главная причина заключается в том, что он сохраняет ваши основные таблицы сущностей чистыми и беспорядочными.

(Приведенные выше данные и пример вымышлены - моя база данных не моделирует сотрудников).

10 голосов
/ 22 сентября 2010

Вот мой предложенный подход, который в прошлом очень хорошо работал для меня:

  • Забудьте номер версии.Вместо этого используйте StartDate и EndDate столбцы
  • . Запишите триггер, чтобы гарантировать отсутствие перекрывающихся диапазонов дат для одного и того же ID и наличие только одной записи с NULL EndDate для того же ID (это ваша текущая действующая запись)
  • Поместите индексы в StartDate и EndDate;это должно дать вам разумную производительность

Это легко позволит вам сообщить по дате:

select *
from MyTable 
where MyReportDate between StartDate and EndDate

или получить текущую информацию:

select *
from MyTable 
where EndDate is null
3 голосов
/ 11 октября 2018

Хотя вопрос задавался 8 лет назад, стоит упомянуть, что в SQL Server 2016 есть именно эта функция. Системная версия временной таблицы

Каждая таблица в SQL Server 2016 и более поздних версиях может иметь таблицу истории, которая будет автоматически заполняться историческими данными самим SQL Server.

Все, что вам нужно, это добавить два столбца datetime2 и одно предложение в таблицу:

CREATE TABLE Employee 
(
    Id int NOT NULL PRIMARY KEY CLUSTERED,
    [Name] varchar(50) NOT NULL,
    Position varchar(50)  NULL,
    Pay money NULL,
    ValidFrom datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
        PERIOD FOR SYSTEM_TIME (ValidFrom,ValidTo)
)  
WITH (SYSTEM_VERSIONING = ON);

Системная версионная таблица создает временную таблицу, в которой хранится история данных. Вы можете использовать произвольное имя WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ) );

В по этой ссылке вы можете найти более подробную информацию о темпоральных таблицах версии системы.

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

2 голосов
/ 23 сентября 2010

Вы определенно делаете это неправильно. Для правильной работы базы данных требуется, чтобы в рабочих таблицах было только минимальное количество данных. Неизбежное хранение исторических данных в режиме реального времени добавляет избыточность, которая усложнит запросы и снизит производительность, плюс ваши преемники будут смотреть на это по-настоящему косо, прежде чем отправлять их в DailyWTF!

Вместо этого создайте копию таблицы - например, EmployeeHistorical - но со столбцом идентификатора, не установленным в качестве идентификатора (вы можете выбрать добавление дополнительного нового столбца идентификатора и столбца отметки времени dateCreated). Затем добавьте в свою таблицу Employee триггер, который запускается при обновлении и удалении и записывает копию полной строки в историческую таблицу. И в то время как у вас есть захват идентификатора пользователя, выполняющего редактирование, часто оказывается полезным для целей аудита.

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

Ваши проблемы с редактированием должны решаться с помощью обычной транзакции базы данных и механизмов блокировки. Кодирование adhoc взламывает, чтобы подражать такому себе, всегда отнимает много времени и подвержено ошибкам (некоторые крайние условия, о которых вы никогда не думали, всегда всплывают, и чтобы правильно писать блокировки, вы действительно должны получить sempahores , что решительно нетривиально)

2 голосов
/ 22 сентября 2010

Идея 3 будет работать:

SELECT * FROM EMPLOYEE AS e1
WHERE Position = 'Coder'
AND Version = (
    SELECT MAX(Version) FROM Employee AS e2
    WHERE e1.ID=e2.ID)

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

EDIT :

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

...