Дизайн базы данных для редакций?

125

У нас есть требование в проекте хранить все ревизии (историю изменений) для объектов в базе данных. На данный момент у нас есть 2 разработанных предложения для этого:

например, для организации "Сотрудник"

Вариант 1:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

Вариант 2:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

Есть ли другой способ сделать это?

Проблема с «Проектом 1» заключается в том, что нам приходится анализировать XML каждый раз, когда вам нужно получить доступ к данным. Это замедлит процесс, а также добавит некоторые ограничения, например, мы не можем добавлять объединения в поля данных ревизий.

И проблема с «Проектом 2» заключается в том, что мы должны дублировать каждое поле для всех сущностей (у нас есть около 70-80 сущностей, для которых мы хотим поддерживать исправления).

Рамеш Сони
источник
3
связанные: stackoverflow.com/questions/9852703/…
Kaii
1
К вашему сведению: на всякий случай, это может помочь. SQL server 2008 и выше имеет технологию, которая показывает историю изменений в таблице. Посетите simple-talk.com/sql/learn-sql-server/…, чтобы узнать больше, и я уверен, что DB вроде Oracle тоже будет что-то подобное.
Durai Amuthan.H
Имейте в виду, что некоторые столбцы могут хранить сами XML или JSON. Если сейчас этого не произойдет, это может произойти в будущем. Лучше убедитесь, что вам не нужно вкладывать такие данные друг в друга.
jakubiszon

Ответы:

38
  1. Как не поставить все это в одной таблице с IsCurrent атрибут дискриминатора. Это просто вызывает проблемы, требует суррогатных ключей и всевозможных других проблем.
  2. В дизайне 2 действительно есть проблемы с изменением схемы. Если вы измените таблицу Employees, вам придется изменить таблицу EmployeeHistories и все связанные с ней sprocs. Потенциально удваивает ваши усилия по изменению схемы.
  3. Дизайн 1 работает хорошо, и при правильном исполнении он не требует больших затрат с точки зрения производительности. Вы можете использовать XML-схему и даже индексы, чтобы преодолеть возможные проблемы с производительностью. Ваш комментарий о синтаксическом анализе xml действителен, но вы можете легко создать представление с помощью xquery, которое вы можете включать в запросы и присоединяться к нему. Что-то вроде этого...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 
Саймон Манро
источник
25
Почему вы говорите не хранить все это в одной таблице с триггером IsCurrent? Не могли бы вы указать мне на несколько примеров, когда это было бы проблематично.
Натан В.
@Simon Munro А что насчет первичного или кластерного ключа? Какой ключ мы можем добавить в таблицу истории Design 1, чтобы ускорить поиск?
gotqn
Я предполагаю простые SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'результаты в полном сканировании таблицы . Не лучшая идея масштабировать приложение.
Kaii
54

Я думаю, что ключевой вопрос, который следует здесь задать, - «Кто / что будет использовать историю»?

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

Создайте таблицу с именем AuditTrail или что-то в этом роде со следующими полями ...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

Затем вы можете добавить столбец LastUpdatedByUserID во все свои таблицы, который должен устанавливаться каждый раз, когда вы выполняете обновление / вставку в таблицу.

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

Мы используем поле RecordID для хранения значения ключевого поля обновляемой таблицы. Если это комбинированный ключ, мы просто выполняем конкатенацию строк со знаком «~» между полями.

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

Просто мысль!

Крис Робертс
источник
5
Нет необходимости хранить NewValue, поскольку оно хранится в проверяемой таблице.
Петрус Терон
17
Собственно говоря, это правда. Но - когда в одно и то же поле в течение определенного периода времени вносится несколько изменений, сохранение нового значения значительно упрощает такие запросы, как «покажите мне все изменения, сделанные Брайаном», поскольку вся информация об одном обновлении хранится в одна запись. Просто мысль!
Крис Робертс,
1
Я думаю, это sysnameможет быть более подходящий тип данных для имен таблиц и столбцов.
Сэм
2
@Sam с использованием sysname не добавляет никакой ценности; это может даже сбивать с толку ... stackoverflow.com/questions/5720212/…
Jowen
19

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

редактировать

В эссе History Tables автор ( Кеннет Даунс ) рекомендует вести историческую таблицу как минимум из семи столбцов:

  1. Отметка времени изменения,
  2. Пользователь, который внес изменение,
  3. Маркер для идентификации записи, которая была изменена (где история ведется отдельно от текущего состояния),
  4. Было ли изменение вставкой, обновлением или удалением,
  5. Старое значение,
  6. Новое значение,
  7. Дельта (для изменения числовых значений).

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

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

Марк Стритфилд
источник
14

Мы реализовали решение, очень похожее на решение, которое предлагает Крис Робертс, и оно нам очень подходит.

Единственная разница в том, что мы сохраняем только новое значение. Старое значение все-таки сохраняется в предыдущей строке истории

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

Допустим, у вас есть таблица с 20 столбцами. Таким образом, вам нужно сохранить только тот столбец, который был изменен, вместо того, чтобы хранить всю строку.

Кьетил Ватнедал
источник
14

Избегайте дизайна 1; это не очень удобно, если вам нужно, например, откатиться к старым версиям записей - автоматически или «вручную» с помощью консоли администратора.

Я не вижу недостатков в Design 2. Я думаю, что вторая таблица History должна содержать все столбцы, присутствующие в первой таблице Records. Например, в mysql вы можете легко создать таблицу с той же структурой, что и другая таблица ( create table X like Y). И когда вы собираетесь изменить структуру таблицы Records в вашей живой базе данных, вам alter tableвсе равно придется использовать команды - и нет больших усилий для выполнения этих команд также для вашей таблицы History.

Ноты

  • Таблица записей содержит только самую последнюю ревизию;
  • Таблица истории содержит все предыдущие ревизии записей в таблице записей;
  • Первичный ключ таблицы истории - это первичный ключ таблицы записей с добавленным RevisionIdстолбцом;
  • Подумайте о дополнительных вспомогательных полях, таких как ModifiedBy- пользователь, создавший конкретную ревизию. Вы также можете захотеть иметь поле DeletedByдля отслеживания того, кто удалил конкретную ревизию.
  • Подумайте, что DateModifiedдолжно означать - либо это означает, где эта конкретная ревизия была создана, либо это будет означать, когда эта конкретная ревизия была заменена другой. Первый требует, чтобы поле находилось в таблице Records, и на первый взгляд кажется более интуитивным; однако второе решение кажется более практичным для удаленных записей (дата, когда была удалена эта конкретная ревизия). Если вы выберете первое решение, вам, вероятно, понадобится второе поле DateDeleted(конечно, только если оно вам нужно). Зависит от вас и от того, что вы действительно хотите записать.

Операции в Варианте 2 очень тривиальны:

Изменить
  • скопируйте запись из таблицы Records в таблицу History, дайте ей новый RevisionId (если он еще не присутствует в таблице Records), обработайте DateModified (зависит от того, как вы его интерпретируете, см. примечания выше)
  • продолжить обычное обновление записи в таблице Records
Удалить
  • сделайте то же самое, что и на первом шаге операции изменения. Обработайте DateModified / DateDeleted соответственно, в зависимости от выбранной вами интерпретации.
Отменить удаление (или откат)
  • возьмите самую высокую (или какую-то конкретную?) ревизию из таблицы History и скопируйте ее в таблицу Records
Список истории изменений для конкретной записи
  • выберите из таблицы истории и таблицы записей
  • подумайте, чего именно вы ждете от этой операции; он, вероятно, определит, какая информация вам нужна из полей DateModified / DateDeleted (см. примечания выше)

Если вы выберете дизайн 2, все команды SQL, необходимые для этого, будут очень простыми, как и обслуживание! Возможно, будет намного проще использовать вспомогательные столбцы ( RevisionId, DateModified) также в таблице Records - чтобы обе таблицы имели одинаковую структуру (за исключением уникальных ключей)! Это позволит использовать простые команды SQL, которые будут устойчивы к любому изменению структуры данных:

insert into EmployeeHistory select * from Employe where ID = XX

Не забывайте использовать транзакции!

Что касается масштабирования , это решение очень эффективно, поскольку вы не преобразуете какие-либо данные из XML туда и обратно, а просто копируете целые строки таблицы - очень простые запросы с использованием индексов - очень эффективно!

TMS
источник
12

Если вам нужно сохранить историю, создайте теневую таблицу с той же схемой, что и таблица, которую вы отслеживаете, и столбцами «Дата ревизии» и «Тип ревизии» (например, «удалить», «обновить»). Напишите (или сгенерируйте - см. Ниже) набор триггеров для заполнения таблицы аудита.

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

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

ConcernedOfTunbridgeWells
источник
3
+1 за простоту! Некоторые будут чрезмерно трудиться из-за страха перед изменениями, в то время как большую часть времени на самом деле никаких изменений не происходит! Кроме того, гораздо проще управлять историей в одной таблице и фактическими записями в другой, чем хранить их все в одной таблице (кошмар) с каким-либо флагом или статусом. Это называется «ПОЦЕЛУЙ» и обычно вознаградит вас в долгосрочной перспективе.
Jeach
+1 полностью согласен, именно то, что я говорю в своем ответе ! Просто и мощно!
TMS
8

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

Employee (Id, Name, ... , IsActive)  

где IsActive - признак последней версии

Если вы хотите связать дополнительную информацию с ревизиями, вы можете создать отдельную таблицу, содержащую эту информацию, и связать ее с таблицами сущностей, используя отношение PK \ FK.

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

  • Простая структура базы данных
  • Никаких конфликтов, поскольку таблица становится доступной только для добавления
  • Вы можете вернуться к предыдущей версии, просто изменив флаг IsActive
  • Нет необходимости в объединениях для получения истории объекта

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

Ака
источник
6
Я бы использовал столбец «RevisionNumber» или «RevisionDate» вместо или в дополнение к IsActive, чтобы вы могли видеть все изменения по порядку.
Sklivvz
Я бы использовал parentRowId, потому что это дает вам легкий доступ к предыдущим версиям, а также возможность быстро найти как основание, так и конец.
chacham15
6

То, как я видел это в прошлом, было

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

Вы никогда не «обновляете» эту таблицу (за исключением того, чтобы изменить действительность isCurrent), просто вставляйте новые строки. Для любого заданного EmployeeId только 1 строка может иметь isCurrent == 1.

Сложность поддержания этого может быть скрыта представлениями и триггерами «вместо» (в Oracle я предполагаю аналогичные вещи в других СУБД), вы даже можете перейти к материализованным представлениям, если таблицы слишком велики и не могут обрабатываться индексами) ,

Этот метод подходит, но вы можете получить несколько сложных запросов.

Лично мне очень нравится ваш способ сделать это в Design 2, как я и делал это раньше. Его просто понять, легко реализовать и просто поддерживать.

Это также создает очень небольшие накладные расходы для базы данных и приложения, особенно при выполнении запросов чтения, что, вероятно, вы будете делать в 99% случаев.

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

Мэтью Уотсон
источник
4

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

Хенрик Густафссон
источник
4

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

введите описание изображения здесь

В этом примере у нас есть объект с именем employee . Таблица user содержит записи ваших пользователей, а entity и entity_revision - две таблицы, которые содержат историю изменений для всех типов сущностей, которые будут у вас в системе. Вот как работает этот дизайн:

Два поля entity_id и revision_id

Каждая сущность в вашей системе будет иметь собственный уникальный идентификатор сущности. Ваша сущность может претерпеть изменения, но ее entity_id останется прежним. Вам необходимо сохранить этот идентификатор объекта в таблице сотрудников (как внешний ключ). Вы также должны сохранить тип своей сущности в таблице сущностей (например, «сотрудник»). Теперь что касается revision_id, как видно из его названия, он отслеживает изменения вашей сущности. Лучший способ, который я нашел для этого, - использовать employee_id в качестве вашего revision_id. Это означает, что у вас будут одинаковые идентификаторы ревизий для разных типов сущностей, но для меня это не проблема (я не уверен насчет вашего случая). Единственное важное замечание: комбинация entity_id и revision_id должна быть уникальной.

Там также состояние поля в entity_revision таблице , которая указывает на состояние пересмотра. Он может иметь одно из трех состояний: latest, obsoleteили deleted(не полагаясь на дату пересмотра поможет вам многое , чтобы повысить свои запросы).

И последнее замечание о revision_id: я не создавал внешний ключ, соединяющий employee_id с revision_id, потому что мы не хотим изменять таблицу entity_revision для каждого типа сущности, который мы могли бы добавить в будущем.

INSERTION

Для каждого сотрудника , которого вы хотите вставить в базу данных, вы также добавите запись в entity и entity_revision . Эти последние две записи помогут вам отслеживать, кем и когда запись была вставлена ​​в базу данных.

ОБНОВИТЬ

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

УДАЛЕНИЕ

Для удаления сотрудника в entity_revision вставляется запись об удалении и завершение.

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

[ОБНОВИТЬ]

Имея поддержку разделов в новых версиях MySQL, я считаю, что мой дизайн также обладает одной из лучших характеристик. Можно разделить entityтаблицу с помощью typeполя, а разделить entity_revisionс помощью ее stateполя. Это значительно увеличит количество SELECTзапросов, сохраняя при этом простой и понятный дизайн.

Mehran
источник
3

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

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

Хэнк Гей
источник
3

Если вы хотите сделать первый, вы можете также использовать XML для таблицы «Сотрудники». Большинство новых баз данных позволяют выполнять запросы к полям XML, поэтому это не всегда проблема. И может быть проще иметь один способ доступа к данным сотрудников, независимо от того, последняя это версия или более ранняя.

Хотя я бы попробовал второй подход. Вы можете упростить это, имея только одну таблицу сотрудников с полем DateModified. EmployeeId + DateModified будет первичным ключом, и вы можете сохранить новую версию, просто добавив строку. Таким образом, архивировать старые версии и восстанавливать версии из архива также проще.

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

Mendelt
источник
2

Как насчет:

  • EmployeeID
  • Дата изменена
    • и / или номер версии, в зависимости от того, как вы хотите ее отслеживать
  • ModifiedByUSerId
    • плюс любая другая информация, которую вы хотите отслеживать
  • Поля сотрудников

Вы делаете первичный ключ (EmployeeId, DateModified), а чтобы получить «текущую» запись (записи), вы просто выбираете MAX (DateModified) для каждого идентификатора сотрудника. Хранение IsCurrent - очень плохая идея, потому что, во-первых, его можно вычислить, а во-вторых, данные слишком легко рассинхронизируются.

Вы также можете создать представление, в котором перечислены только последние записи, и в основном использовать его при работе в приложении. Хорошая вещь в этом подходе заключается в том, что у вас нет дубликатов данных, и вам не нужно собирать данные из двух разных мест (текущих в Employees и заархивированных в EmployeesHistory), чтобы получить всю историю или откат и т. Д.) ,

gregmac
источник
Недостатком этого подхода является то, что таблица будет расти быстрее, чем если бы вы использовали две таблицы.
cdmckay
2

Если вы хотите полагаться на данные истории (для отчетов), вы должны использовать такую ​​структуру:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

Или глобальное решение для применения:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

Вы можете сохранить свои ревизии также в XML, тогда у вас будет только одна запись для одной ревизии. Это будет выглядеть так:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"
dariol
источник
1
Лучше: используйте источник событий :)
дариол 06
1

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

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

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

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

mattruma
источник
0

Похоже, вы хотите отслеживать изменения определенных объектов с течением времени, например, ID 3, «bob», «123 main street», затем еще ID 3, «bob», «234 elm st» и т. Д., По сути, имея возможность чтобы вырвать историю изменений, показывающую каждый адрес, по которому был "bob".

Лучший способ сделать это - иметь поле «текущее» в каждой записи и (возможно) временную метку или FK в таблице даты / времени.

Затем вставки должны установить «является текущим», а также сбросить «текущее значение» для предыдущей «текущей» записи. В запросах должно быть указано «текущее», если вам не нужна вся история.

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

Стив Мун
источник