Чи є параметр / функція MySQL для відстеження історії змін у записах?


122

Мене запитали, чи можу я відстежувати зміни в записах у базі даних MySQL. Отже, коли поле було змінено, доступне старе та нове, а дата відбулася. Чи є особливість чи загальна техніка для цього?

Якщо так, я думав зробити щось подібне. Створіть таблицю під назвою changes. Він містив би ті самі поля, що і головна таблиця, але з префіксом старого та нового, але лише для тих полів, які були фактично змінені, і TIMESTAMPдля нього. Це було б індексовано а ID. Таким чином, можна створити SELECTзвіт, який відображатиме історію кожного запису. Це хороший метод? Дякую!

Відповіді:


83

Це тонко.

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

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

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

Наприклад, якщо у вас є така таблиця:

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

і ви хотіли слідкувати за часом, ви внесете зміни до цього тексту:

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

Кожен раз, коли ви хочете змінити запис клієнта, замість оновлення запису, ви встановлюєте VALID_UNTIL у поточному записі на NOW () і вставляєте новий запис з VALID_FROM (зараз) та нульовим VALID_UNTIL. Ви встановлюєте статус "CUSTOMER_USER" на ідентифікаційний номер входу поточного користувача (якщо вам потрібно це зберегти). Якщо клієнта потрібно видалити, ви використовуєте прапор CUSTOMER_STATUS для позначення цього - ви ніколи не можете видаляти записи з цієї таблиці.

Таким чином, ви завжди можете дізнатися, яким був статус таблиці клієнтів для даної дати - яка адреса? Чи змінили ім’я? Приєднавшись до інших таблиць із подібними датами valid_from та valid_until, ви можете реконструювати всю картину історично. Щоб знайти поточний статус, ви шукаєте записи з нульовою датою VALID_UNTIL.

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


Але це додало б повторювані дані для тих полів, які не оновлюються? Як керувати ним?
itzmukeshy7

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

Найкраща пропозиція, яку я бачив до цього часу,
Варто7

О, і у відповідь на коментарі, як щодо просто збереження нуля для всього іншого, що не змінилося? Таким чином, найновіша версія буде усіма останніми даними, але якщо ім'я раніше було "Боб" 5 днів тому, тоді просто один рядок, ім'я = bob та дійсне до 5 днів тому.
Варто7

2
Поєднання customer_id та дати є основним ключем, тому вони будуть гарантовано унікальними.
Невілл Куйт

186

Ось простий спосіб зробити це:

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

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

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

Таблицю історії створити досить просто. У запиті ALTER TABLE нижче (і в тригерних запитах нижче цього) замініть "basic_key_column" фактичною назвою цього стовпця в таблиці даних.

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

І тоді ви створюєте тригери:

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

І ви закінчили. Тепер усі вставки, оновлення та видалення у "MyDb.data" будуть записані у "MyDb.data_history", даючи вам таку історію, як ця (за вирахуванням надуманого стовпця "data_column")

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

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

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

Редагувати: О, о, людям подобається річ з моєї таблиці історії з 6 років тому: P

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

Щоб звертатися до деяких коментарів у конкретному порядку:

  • Я зробив власну реалізацію в PHP, яка була трохи більше задіяна, і уникнула деяких проблем, описаних у коментарях (перенесення індексів помітно. Якщо ви перенесете унікальні індекси в таблицю історії, все порушиться. Є рішення для це в коментарях). Слідування за цим повідомленням до листа може стати пригодою, залежно від того, наскільки створена ваша база даних.

  • Якщо взаємозв'язок між первинним ключем і стовпцем ревізії здається вимкненим, це зазвичай означає, що складений ключ якось замикається. У кількох рідкісних випадках у мене траплялося таке, і я втрачав причину.

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

  • якщо ви отримуєте повторні вставки, перевірте свій програмний рівень на наявність запитів типу INSERT IGNORE. Hrmm, не можу зараз згадати, але я думаю, що є проблеми з цією схемою та транзакції, які в кінцевому рахунку виходять з ладу після виконання декількох дій DML. Щось слід пам’ятати, принаймні.

  • Важливо, щоб поля в таблиці історії та таблиці даних співпадали. Або, вірніше, у вашій таблиці даних немає БОЛЬШІ стовпців, ніж у таблиці історії. В іншому випадку запити вставити / оновити / del у таблицю даних не вдасться, коли вставки до таблиць історії помістять стовпці в запит, який не існує (через d * в запитах тригера), і тригер не працює. t було б дивним, якби MySQL мав щось подібне до тригерів схеми, де ви могли б змінити таблицю історії, якби стовпці були додані до таблиці даних. Чи є у MySQL це зараз? Я реагую в ці дні: P


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

1
Нещодавно я зіткнувся з проблемою використання цього рішення для проекту, через те, як всі індекси з початкової таблиці копіюються в таблицю історії (завдяки тому, як CREATE TABLE ... LIKE .... працює). Наявність унікальних індексів у таблиці історії може спричинити запит ВСТАВИТИ в триггер ПІСЛЯ ОНОВЛЕННЯ до барфа, тому їх потрібно видалити. У скрипті php, який у мене є цей матеріал, я запитую будь-які унікальні індекси на новостворених таблицях історії (з "SHOW INDEX FROM data_table WHERE Key_name! =" PRIMARY "та Non_unique = 0"), а потім видаляю їх.
тимчасове закриття

3
Тут ми отримуємо неодноразові дані, що вставляються в резервну таблицю кожного разу. Нехай, якщо у нас є 10 полів у таблиці, а ми оновили 2, тоді ми додаємо повторні дані для решти 8 полів. Як перемогти з цього?
itzmukeshy7

6
Ви можете уникнути випадкового перенесення різних індексів, змінивши оператор створення таблиці наCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Ерік Хейз

4
@transientclosure Як би ви запропонували отримати інші поля в історії, які не були частиною оригінального запиту? наприклад, я хочу відстежити, хто вносить ці зміни. для вставки у нього вже є ownerполе, і для оновлення я можу додати updatedbyполе, але для видалення я не впевнений, як це можна зробити через тригери. оновлення data_historyрядка з ідентифікатором користувача відчувається брудним: P
Кінь

16

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

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

Тривалий час я копіював інформацію в іншу таблицю за допомогою сценарію, оскільки MySQL не підтримував тригери на той час. Зараз я виявив, що цей тригер є більш ефективним у відстеженні всього.

Цей тригер скопіює старе значення в таблицю історії, якщо воно буде змінено, коли хтось редагує рядок. Editor IDі last modзберігаються в оригінальній таблиці щоразу, коли хтось редагує цей рядок; час відповідає часу, коли він був змінений на його сучасну форму.

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

Іншим рішенням буде зберегти поле Revision і оновити це поле на збереженні. Ви можете вирішити, що макс - це остання редакція, або що 0 - це останній ряд. Це залежить від вас.


9

Ось як ми її вирішили

a Таблиця користувачів виглядала так

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

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

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

Щоб знайти поточну адресу будь-якого користувача, ми шукаємо UserData з редакцією DESC та LIMIT 1

Щоб отримати адресу користувача між певним періодом часу, ми можемо використовувати create_on bewteen (дата1, дата 2)


Я хочу це рішення, але я хочу знати, як можна вставити id_user в цю таблицю за допомогою тригера?
Thecassion

1
Що сталося з revision=1з id_user=1? Спочатку я подумав, що ваш підрахунок, 0,2,3,...але потім я побачив, що для id_user=2перегляду підрахунок0,1, ...
Pathros,

1
Вам не потрібні idі ідентифікатори id_userстовпців " . Just use a group ID of (ідентифікатор користувача) та" revision.
Гаджус

6

MariaDB підтримує версію системи з 10.3, яка є стандартною функцією SQL, яка робить саме те, що ви хочете: вона зберігає історію записів таблиць і забезпечує доступ до неї через SELECT запитів. MariaDB - це вилка відкритої розробки MySQL. Більше про його версію системи можна знайти за цим посиланням:

https://mariadb.com/kb/en/library/system-versioned-tables/


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

4

Чому б просто не використовувати файли журналу бін? Якщо реплікація встановлена ​​на сервері Mysql, а формат бінарного файлу встановлено як ROW, то всі зміни можуть бути зафіксовані.

Можна використовувати гарну бібліотеку пітонів під назвою noplay. Більше інформації тут .


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

3

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

Моя таблиця змін буде просто:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1) Коли в головній таблиці буде змінено весь рядок, у цю таблицю буде вписано багато записів, АЛЕ це дуже малоймовірно, тому не велика проблема (люди зазвичай змінюють лише одне) 2) OldVaue (і NewValue, якщо ви Хочете) повинні бути якимось епічним "будь-яким типом", оскільки це можуть бути будь-які дані, може бути спосіб це зробити з типами RAW або просто використовувати рядки JSON для перетворення і виходу.

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

Для створення та видалення лише ідентифікатор рядка, поля не потрібні. При видаленні прапора з головної таблиці (активний?) Було б добре.


0

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

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

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

Для цього ми також можемо використовувати інструмент 3-ї частини. Тут я пишу програму Java Mysql Tracker


як я можу використовувати ваш Mysql Tracker?
webchun

1
1. Переконайтеся, що у вас є стовпець id як основний ключ у кожній таблиці. 2. Скопіюйте файл java в локальний (або IDE) 3. Імпортуйте lib і відредагуйте статичні змінні з рядка 9-15 відповідно до конфігурації та структури вашої бази даних. 4. Проаналізуйте та запустіть файл java 5. Скопіюйте журнал консолі та виконайте його як команди Mysql
goforu

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