Дизайн бази даних для ревізій?


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 об'єктів, для яких ми хочемо виконати зміни).



1
FYI: Тільки в разі , якщо це може допомогти .SQL сервер 2008 і вище , має технологію , яка показує історію змін на table..visit simple-talk.com/sql/learn-sql-server / ... знати більше і я впевнений , що децибел як у Oracle також буде щось подібне.
Дурай Амутан.H

Зверніть увагу, що деякі стовпці можуть зберігати самі XML або JSON. Якщо це не так, то це може статися в майбутньому. Краще переконайтеся, що вам не потрібно вкладати такі дані один в інший.
jakubiszon

Відповіді:


38
  1. Не треба поставити все це в одній таблиці з IsCurrent атрибут дискриминатора. Це просто спричиняє проблеми вниз, вимагає сурогатних ключів та всіляких інших проблем.
  2. Дизайн 2 має проблеми зі змінами схеми. Якщо ви змінюєте таблицю Employees, вам слід змінити таблицю EmployeeHistories та всі пов’язані з цим відростки. Потенційно вдвічі ви намагаєтеся змінити схему.
  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. Не могли б ви вказати мені кілька прикладів, коли це стане проблематичним.
Nathan W

@Simon Munro Щодо первинного ключа або кластерного ключа? Який ключ ми можемо додати до таблиці історії дизайну 1, щоб зробити пошук швидшим?
gotqn

Я припускаю прості SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'результати при повному скануванні таблиці . Не найкраща ідея для масштабування програми.
Каї

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

Стаття "Таблиці історії" в блозі " Програміст баз даних" може бути корисною - висвітлює деякі моменти, порушені тут, та обговорює збереження дельти.

Редагувати

У нарисі « Історичні таблиці» автор ( Кеннет Даунс ) рекомендує підтримувати таблицю історії щонайменше із семи стовпців:

  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; це не дуже зручно, коли вам потрібно буде, наприклад, відкат до старих версій записів - автоматично або «вручну» за допомогою консолі адміністраторів.

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

Примітки

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

Операції в дизайні 2 дуже тривіальні:

Змінити
  • скопіюйте запис із таблиці Records у таблицю History, надайте йому новий RevisionId (якщо він ще не присутній у таблиці Records), обробіть DateModified (залежить від способу інтерпретації, див. примітки вище)
  • продовжуйте звичайне оновлення запису в таблиці Records
Видалити
  • зробіть точно так само, як і на першому кроці операції Змінити. Обробіть DateModified / DateDeleted відповідно, залежно від інтерпретації, яку ви обрали.
Скасувати видалення (або відкат)
  • зробіть найвищу (чи якусь конкретну?) версію з таблиці "Історія" та скопіюйте її в таблицю "Записи"
Перерахуйте історію редагування для конкретного запису
  • виберіть із таблиці Історія та Таблиця записів
  • подумайте, чого саме ви очікуєте від цієї операції; це, ймовірно, визначить, яку інформацію вам потрібно з полів DateModified / DateDeleted (див. примітки вище)

Якщо ви перейдете на Design 2, всі команди SQL, необхідні для цього, будуть дуже прості, а також обслуговування! Можливо, буде набагато простіше, якщо ви будете використовувати допоміжні стовпці ( RevisionId, DateModified) також у таблиці Records - тримати обидві таблиці точно в одній структурі (за винятком унікальних ключів)! Це дозволить отримати прості команди SQL, які будуть толерантними до будь-яких змін структури даних:

insert into EmployeeHistory select * from Employe where ID = XX

Не забудьте використовувати транзакції!

Що стосується масштабування , це рішення є дуже ефективним, оскільки ви не перетворюєте жодні дані з XML назад і назад, просто копіюючи цілі рядки таблиці - дуже прості запити, використовуючи індекси - дуже ефективно!


12

Якщо вам потрібно зберігати історію, складіть тіньову таблицю з тією ж схемою, що і таблиця, яку ви відстежуєте, та стовпці "Дата перегляду" та "Тип редакції" (наприклад, "видалити", "оновити"). Напишіть (або генеруйте - див. Нижче) набір тригерів для заповнення таблиці аудиту.

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

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


3
+1 для простоти! Деякі з них будуть надмірно інженерними, боячись пізніших змін, тоді як більшість часу насправді ніяких змін не відбувається! Крім того, набагато простіше керувати історіями в одній таблиці та фактичними записами в іншій, ніж мати їх усі в одній таблиці (кошмар) з деяким прапором або статусом. Це називається "KISS" і, як правило, винагородить вас у довгостроковій перспективі.
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), просто вставляйте нові рядки. Для будь-якого даного EmployeeeId лише 1 рядок може мати isCurrent == 1.

Складність підтримання цього може бути прихована за допомогою переглядів і "замість" тригерів (в Oracle, я припускаю подібні речі в інших RDBMS), ви навіть можете перейти до матеріалізованих представлень, якщо таблиці занадто великі і їх не можна обробляти за допомогою індексів) .

Цей спосіб нормальний, але ви можете закінчити деякі складні запити.

Особисто мені дуже подобається твій спосіб дизайну 2, як це я робив і в минулому. Його просто зрозуміти, простий у здійсненні та простий у обслуговуванні.

Це також створює дуже невеликі накладні витрати для бази даних та додатків, особливо при виконанні запитів читання, що, ймовірно, ви будете робити 99% часу.

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


4

Перегляд даних - це аспект концепції " дійсного часу " тимчасової бази даних. У цьому пройшло багато досліджень, і з'явилося багато закономірностей та настанов. Я написав довгу відповідь з купою посилань на це питання для зацікавлених.


4

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

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

У цьому прикладі ми маємо юридичну особу на ім’я співробітник . користувальницька таблиця містить записи ваших користувачів, а сутність та сутність_ревізії - це дві таблиці, що містять історію ревізій для всіх типів об'єктів, які ви матимете у вашій системі. Ось як працює ця конструкція:

Два поля entit_id та revision_id

Кожна сутність у вашій системі матиме свій унікальний ідентифікатор сутності. Ваша організація може пройти зміни, але її_мення_код залишиться тією самою. Потрібно зберегти цей ідентифікатор сутності у вашій таблиці працівника (як зовнішній ключ). Ви також повинні зберігати тип вашої організації в таблиці сутності (наприклад, "співробітник"). Тепер, що стосується revivision_id, як показує його назва, він відслідковує зміни вашої сутності. Найкращий спосіб, який я знайшов для цього, - це використовувати служитель_id як свою ревізію. Це означає, що у вас будуть дублікати ідентифікаторів редакції для різних типів організацій, але це не стосується мене (я не впевнений у вашому випадку). Єдине важливе зауваження, яке слід зробити, - це те, що комбінація entit_id та revision_id має бути унікальною.

Там також стан поля в entity_revision таблиці , яка вказує на стан перегляду. Він може мати одне з трьох станів: latest, obsoleteабоdeleted (не покладаючись на дату перегляду допоможе вам багато , щоб підвищити свої запити).

Останнє зауваження про revivision_id, я не створив зовнішній ключ, що з'єднує_захисний_в_завдання до ревізіонного_, тому що ми не хочемо змінювати таблицю сутностей_ревізії для кожного типу об'єкта, який ми могли б додати в майбутньому.

ВСТУП

Для кожного співробітника, якого ви хочете вставити в базу даних, ви також додасте запис до entity та entit_revision . Ці два останні записи допоможуть вам відстежувати, хто і коли запис вставив у базу даних.

ОНОВЛЕННЯ

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

ВИДАЛЕННЯ

Для видалення працівника запис у_ревізію вставляється запис, в якому зазначається про видалення та зроблено.

Як ви бачите в цьому дизайні, жодні дані ніколи не змінюються і не видаляються з бази даних, і що ще важливіше, кожен тип сутності вимагає лише однієї таблиці. Особисто я вважаю цей дизайн дійсно гнучким і простим у роботі. Але я не впевнений у вас, оскільки ваші потреби можуть бути різними.

[ОНОВЛЕННЯ]

Підтримуючи розділи в нових версіях MySQL, я вважаю, що моя конструкція також має одне з найкращих показників. entityТаблицю можна розділити, використовуючи typeполе, тоді як розділ entity_revisionвикористовує його stateполе. Це збільшить SELECTзапити набагато, при цьому збереже дизайн і простий дизайн.


3

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

Якщо вам потрібно відслідковувати фактичні історичні (та / або майбутні) версії, то стандартним рішенням є відстеження одного і того ж об'єкта кількома рядками, використовуючи деяку комбінацію значень початку, кінця та тривалості. Ви можете використовувати перегляд, щоб зробити доступ до поточних значень зручним. Якщо це такий підхід, який ви можете взяти, ви можете зіткнутися з проблемами, якщо ваші посилання на різні дані можуть змінюватися, але неперевершувати дані.


3

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

Я б спробував другий підхід. Ви можете спростити це, маючи лише одну таблицю співробітників із полем DateModified. EmployeeII + DateModified буде основним ключем, і ви можете зберегти нову редакцію, просто додавши рядок. Таким чином, простіше архівувати старіші версії та відновлювати версії з архіву.

Іншим способом зробити це могла б модель вантажу даних Дена Лінстедта. Я зробив проект для голландського бюро статистики, який використовував цю модель, і вона працює досить добре. Але я не думаю, що це безпосередньо корисно для щоденного використання бази даних. Ви можете отримати деякі ідеї, прочитавши його документи.


2

Як щодо:

  • ID працівника
  • Дата модифікована
    • та / або номер редакції, залежно від того, як ви хочете відстежувати його
  • ModifiedByUSerId
    • плюс будь-яку іншу інформацію, яку ви хочете відстежувати
  • Поля працівників

Ви робите первинний ключ (EmployeeII, DateModified), і щоб отримати "поточний" запис, ви просто вибираєте MAX (DateModified) для кожного співробітника. Зберігання IsCurrent - дуже погана ідея, оскільки, по-перше, його можна обчислити, по-друге, далеко не так легко вийти з даних синхронізації.

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


Недолік цього підходу полягає в тому, що таблиця буде рости швидше, ніж якщо ви використовуєте дві таблиці.
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)"

1
Краще: використовуйте джерела подій :)
dariol

1

У нас були подібні вимоги, і ми виявили, що користувач часто хоче просто бачити що було змінено, а не обов'язково відкручувати будь-які зміни.

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

Щоразу, коли користувач зберігає свої зміни, ми перезавантажуємо старий об'єкт, проводимо порівняння, записуємо зміни та зберігаємо сутність (все робиться в одній транзакції з базою даних, якщо є якісь проблеми).

Це, здається, працює дуже добре для наших користувачів і позбавляє нас від головного болю від створення абсолютно окремої таблиці аудиту з тими ж полями, що і наш суб’єкт господарювання.


0

Це здається, що ви хочете відслідковувати зміни певних об'єктів у часі, наприклад, ID 3, "bob", "123 main street", потім інший ID 3, "bob" "234 elm st", і так далі, по суті зможете щоб вимкнути історію редагування, показуючи кожну адресу "bob".

Найкращий спосіб зробити це - мати поле "поточне" на кожному записі та (ймовірно) часову позначку або FK до таблиці дати / часу.

Потім вставки повинні встановити "поточний", а також зняти "поточний" на попередній "поточний" запис. У запитах потрібно вказати "поточний", якщо ви не хочете всю історію.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.