Есть ли опция / функция MySQL для отслеживания истории изменений записей?


122

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

Если так, то я думал сделать что-то подобное. Создайте таблицу с именем changes. Она будет содержать те же поля, что и основная таблица, но с префиксом old и new, но только для тех полей, которые были фактически изменены, и TIMESTAMPдля них. Он будет проиндексирован с расширением ID. Таким образом, можно создать SELECTотчет для отображения истории каждой записи. Это хороший метод? Спасибо!

Ответы:


83

Это тонко.

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

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

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

Например, если у вас есть такая таблица:

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

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

Лучшее предложение, которое я когда-либо видел по этой проблеме
Worthy7

О, и в ответ на комментарии, как насчет того, чтобы просто сохранить null для всего остального, что не изменилось? Таким образом, самой последней версией будут все самые последние данные, но если имя было «Bob» 5 дней назад, тогда будет только одна строка, name = bob и действительна до 5 дней назад.
Worthy7

2
Комбинация customer_id и дат является первичным ключом, поэтому они гарантированно уникальны.
Невилл

186

Вот простой способ сделать это:

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

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

Для выполнения этого поведения последовательности создается двухколоночный (составной) индекс для столбца первичного ключа и столбца ревизии. Обратите внимание, что вы можете выполнять последовательность таким образом, только если движком, используемым таблицей истории, является MyISAM ( см. «Примечания MyISAM» на этой странице)

Таблицу истории создать довольно просто. В запросе ALTER TABLE ниже (и в запросах триггеров ниже) замените primary_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_columns).

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. Хммм, сейчас не могу вспомнить, но я думаю, что есть проблемы с этой схемой и транзакциями, которые в конечном итоге терпят неудачу после выполнения нескольких действий DML. По крайней мере, кое-что, о чем нужно знать.

  • Важно, чтобы поля в таблице истории и таблице данных совпадали. Или, скорее, ваша таблица данных не имеет БОЛЬШЕ столбцов, чем таблица истории. В противном случае запросы вставки / обновления / удаления в таблице данных завершатся ошибкой, когда вставки в таблицы истории помещают столбцы в запрос, которые не существуют (из-за d. * В запросах триггера), и триггер не работает. Было бы здорово, если бы в MySQL было что-то вроде схем-триггеров, в которых вы могли бы изменить таблицу истории, если бы столбцы были добавлены в таблицу данных. Есть ли это в MySQL сейчас? Сейчас я React: P


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

1
Недавно я столкнулся с проблемой при использовании этого решения для проекта из-за того, что все индексы из исходной таблицы копируются в таблицу истории (из-за того, как работает CREATE TABLE ... LIKE ....). Наличие уникальных индексов в таблице истории может привести к тому, что запрос INSERT в триггере AFTER UPDATE будет отключен, поэтому их необходимо удалить. В имеющемся у меня php скрипте, который делает это, я запрашиваю любые уникальные индексы во вновь созданных таблицах истории (с «SHOW INDEX FROM data_table WHERE Key_name! = 'PRIMARY' и Non_unique = 0»), а затем удаляю их.
временное закрытие

3
Здесь мы каждый раз получаем повторяющиеся данные, вставленные в резервную таблицу. Пусть, если у нас есть 10 полей в таблице и мы обновили 2, то мы добавляем повторяющиеся данные для остальных 8 полей. Как побороться с этим?
itzmukeshy7

6
Вы можете избежать случайного переноса различных индексов, изменив оператор create table наCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes

4
@transientclosure, как бы вы предложили добавить в историю другие поля, которые не были частью исходного запроса? например, я хочу отслеживать, кто вносит эти изменения. для вставки у него уже есть ownerполе, и для обновления я мог бы добавить updatedbyполе, но для удаления я не уверен, как я могу это сделать с помощью триггеров. обновление data_historyстроки с идентификатором пользователя кажется грязным: P
Horse

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;
$$

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


9

Вот как мы это решили

таблица пользователей выглядела так

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.

Чтобы получить адрес пользователя между определенным периодом времени, мы можем использовать created_on bewteen (date1, date 2)


Это решение, которое я хочу иметь, но хочу знать. Как можно вставить id_user в эту таблицу с помощью триггера?
thecassion

1
Что случилось с revision=1из id_user=1? Сначала я подумал, что ваш подсчет был, 0,2,3,...но потом я увидел, что id_user=2подсчет исправлений0,1, ...
Патрос,

1
Вам не нужны idи id_userстолбцы . Just use a group ID of id` (идентификатор пользователя) и revision.
Gajus

6

MariaDB поддерживает управление версиями системы, начиная с 10.3. Это стандартная функция SQL, которая делает именно то, что вы хотите: хранит историю записей таблиц и предоставляет доступ к ней через SELECTзапросы. MariaDB - это открытая разработка MySQL. Вы можете узнать больше об управлении версиями системы по этой ссылке:

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


Обратите внимание на следующее из приведенной выше ссылки: «mysqldump не считывает исторические строки из версионных таблиц, поэтому архивные данные не будут скопированы. Кроме того, восстановление временных меток будет невозможно, поскольку они не могут быть определены с помощью вставки / Пользователь."
Даниэль

4

Почему бы просто не использовать файлы журналов bin? Если репликация настроена на сервере Mysql, а формат файла binlog установлен на ROW, то все изменения могут быть зафиксированы.

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


2
Binlog можно использовать, даже если у вас нет / не требуется репликация. У Binlog много полезных вариантов использования. Репликация, вероятно, является наиболее распространенным вариантом использования, но ее также можно использовать для резервного копирования и журнала аудита, как упоминалось здесь.
webaholik

3

Только мои 2 цента. Я бы создал решение, которое точно записывает, что изменилось, очень похоже на временное решение.

Моя таблица изменений была бы простой:

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

1) Когда в основной таблице изменяется вся строка, в эту таблицу попадет много записей, НО это очень маловероятно, поэтому не большая проблема (люди обычно меняют только одну вещь) 2) OldVaue (и NewValue, если вы want) должны быть своего рода эпическим "любым типом", поскольку это могут быть любые данные, может быть способ сделать это с помощью типов RAW или просто использовать строки JSON для преобразования в и из.

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

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


0

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

Но самое главное, что если у нас будет много столбцов и много таблиц. Мы должны ввести имя каждого столбца каждой таблицы. Очевидно, это пустая трата времени.

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

Мы также можем использовать сторонний инструмент, просто чтобы сделать это. Здесь я пишу Java-программу Mysql Tracker


как я могу использовать ваш Mysql Tracker?
webchun

1
1. Убедитесь, что у вас есть столбец id в качестве первичного ключа в каждой таблице. 2. Скопируйте файл java в локальный (или IDE) 3. Импортируйте библиотеки и отредактируйте статические переменные из строк 9-15 в соответствии с конфигурацией и структурой вашей базы данных. 4. Разберите и запустите файл java. 5. Скопируйте журнал консоли и выполните его как команды Mysql
goforu

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