레코드 변경 내역을 추적하는 MySQL 옵션 / 기능이 있습니까?


122

MySQL 데이터베이스의 레코드 변경 사항을 추적 할 수 있는지 물었습니다. 따라서 필드가 변경되면 이전과 새 항목을 사용할 수 있으며이 날짜가 발생했습니다. 이를 수행하는 기능이나 일반적인 기술이 있습니까?

그렇다면 나는 이와 같은 일을 할 생각이었다. 라는 테이블을 만듭니다 changes. 마스터 테이블 과 동일한 필드를 포함 하지만 이전 및 새 접두사가 붙지 만 실제로 변경된 필드와 해당 필드에만 해당됩니다 TIMESTAMP. 인덱싱됩니다 ID. 이렇게하면 SELECT각 레코드의 기록을 표시 하는 보고서를 실행할 수 있습니다. 이것이 좋은 방법입니까? 감사!

답변:


83

미묘합니다.

비즈니스 요구 사항이 "데이터 변경 사항을 감사하고 싶습니다-누가 언제 무엇을 했습니까?"인 경우 일반적으로 감사 테이블을 사용할 수 있습니다 (Keethanjan이 게시 한 트리거 예제에 따라). 저는 트리거를 좋아하지는 않지만 구현하는 데 비교적 고통이 없다는 큰 이점이 있습니다. 기존 코드는 트리거 및 감사 항목에 대해 알 필요가 없습니다.

비즈니스 요구 사항이 "과거에 주어진 날짜의 데이터 상태 표시"인 경우 시간에 따른 변화의 측면이 솔루션에 들어왔다는 의미입니다. 감사 테이블을 보는 것만으로 데이터베이스의 상태를 재구성 할 수는 있지만 어렵고 오류가 발생하기 쉬우 며 복잡한 데이터베이스 논리의 경우 다루기가 어려워집니다. 예를 들어, 기업에서 "매월 1 일에 미납 된 미납 송장이있는 고객에게 보내야하는 편지의 주소를 찾으려면"6 개의 감사 테이블을 검색해야합니다.

대신 시간에 따른 변화의 개념을 스키마 디자인에 적용 할 수 있습니다 (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 (현재) 및 null VALID_UNTIL을 사용하여 새 레코드를 삽입합니다. "CUSTOMER_USER"상태를 현재 사용자의 로그인 ID로 설정합니다 (유지해야하는 경우). 고객을 삭제해야하는 경우 CUSTOMER_STATUS 플래그를 사용하여이를 표시합니다.이 테이블에서 레코드를 삭제할 수 없습니다.

이렇게하면 주어진 날짜에 대한 고객 테이블의 상태를 항상 찾을 수 있습니다. 주소는 무엇입니까? 이름이 바뀌 었나요? valid_from 및 valid_until 날짜가 유사한 다른 테이블에 조인하여 전체 그림을 역사적으로 재구성 할 수 있습니다. 현재 상태를 찾으려면 VALID_UNTIL 날짜가 null 인 레코드를 검색합니다.

다루기 어렵습니다 (엄격히 말하면 valid_from이 필요하지 않지만 쿼리가 조금 더 쉬워집니다). 디자인과 데이터베이스 액세스가 복잡해집니다. 그러나 그것은 세상을 훨씬 더 쉽게 재구성 할 수있게합니다.


그러나 업데이트되지 않은 필드에 대해 중복 데이터를 추가합니까? 그것을 관리하는 방법?
— itzmukeshy7

두 번째 접근 방식에서는 고객 레코드가 시간이 지남에 따라 편집되는 경우 보고서 생성에 문제가 발생하여 특정 항목이 동일한 고객에 속하는지 다른지 여부를 인식하기가 어렵습니다.
— 하기 Akshay 조시

이 문제에 대해 내가 본 최고의 제안
— Worthy7

아 그리고 댓글에 대한 응답으로 변경되지 않은 다른 모든 항목에 대해 null을 저장하는 것은 어떻습니까? 따라서 최신 버전은 모든 최신 데이터가 될 것이지만 이름이 5 일 전에 "Bob"이었던 경우에는 하나의 행, name = bob 만 있고 5 일 전까지 유효합니다.
— Worthy7

2
customer_id와 날짜의 조합이 기본 키이므로 고유하게 보장됩니다.
— Neville Kuyt

186

이를 수행하는 간단한 방법은 다음과 같습니다.

먼저 추적하려는 각 데이터 테이블에 대한 기록 테이블을 만듭니다 (아래 쿼리 예). 이 테이블에는 데이터 테이블의 각 행에서 수행되는 각 삽입, 업데이트 및 삭제 쿼리에 대한 항목이 있습니다.

히스토리 테이블의 구조는 3 개의 추가 열을 제외하고 추적하는 데이터 테이블과 동일합니다 : 발생한 작업을 저장하는 열 ( 'action'이라고합시다), 작업 날짜 및 시간, 열 작업마다 증가하고 데이터 테이블의 기본 키 열로 그룹화되는 시퀀스 번호 ( '개정')를 저장합니다.

이 시퀀싱 동작을 수행하기 위해 기본 키 열과 개정 열에 두 개의 열 (복합) 인덱스가 생성됩니다. 히스토리 테이블에서 사용하는 엔진이 MyISAM 인 경우에만이 방식으로 시퀀싱을 수행 할 수 있습니다 ( 이 페이지의 'MyISAM Notes'참조).

히스토리 테이블은 생성하기가 매우 쉽습니다. 아래의 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

내 구현은 여전히 ​​윙윙 거리며 점점 커지고 다루기 어려워집니다. 이 데이터베이스의 히스토리를보기 위해 뷰와 꽤 멋진 UI를 작성했지만 많이 사용 된 적이 없다고 생각합니다. 그래서 간다.

특정 순서없이 일부 의견을 처리하려면 :

  • 나는 PHP에서 조금 더 관련이있는 내 자신의 구현을 수행했고 주석에 설명 된 문제 중 일부를 피했습니다 (인덱스가 크게 전송되었습니다. 고유 인덱스를 히스토리 테이블로 전송하면 문제가 발생합니다. 이것은 주석에서). 이 포스트를 따르는 것은 데이터베이스가 어떻게 구축되었는지에 따라 모험이 될 수 있습니다.

  • 기본 키와 개정 열 사이의 관계가 꺼져있는 것처럼 보이면 일반적으로 복합 키가 어떻게 든 지루함을 의미합니다. 드물게 저는 이런 일이 발생하여 원인을 잃었습니다.

  • 이 솔루션은 트리거를 그대로 사용하여 성능이 매우 우수하다는 것을 알았습니다. 또한 MyISAM은 삽입 속도가 빠르며 모든 트리거가 수행합니다. 스마트 인덱싱 (또는 부족한 ...)을 통해이를 더욱 개선 할 수 있습니다. 기본 키를 사용하여 MyISAM 테이블에 단일 행을 삽입하는 것은 다른 곳에서 중요한 문제가 발생하지 않는 한 실제로 최적화해야하는 작업이 아닙니다. 이 히스토리 테이블 구현이있는 MySQL 데이터베이스를 실행하는 동안 내내 발생한 (많은) 성능 문제의 원인은 아닙니다.

  • 반복적으로 삽입되는 경우 소프트웨어 계층에서 INSERT IGNORE 유형 쿼리를 확인하십시오. 흠, 지금은 기억이 나지 않지만 여러 DML 작업을 실행 한 후 궁극적으로 실패하는이 체계와 트랜잭션에 문제가 있다고 생각합니다. 적어도 알아야 할 것.

  • 기록 테이블과 데이터 테이블의 필드가 일치하는 것이 중요합니다. 또는 데이터 테이블에 히스토리 테이블보다 더 많은 열이 없습니다. 그렇지 않으면 데이터 테이블에 대한 삽입 / 업데이트 / 삭제 쿼리가 실패하고, 기록 테이블에 대한 삽입이 존재하지 않는 쿼리에 열을 넣고 (트리거 쿼리의 d. *로 인해) 트리거가 실패합니다. MySQL에 스키마 트리거와 같은 것이 있으면 데이터 테이블에 열이 추가되면 히스토리 테이블을 변경할 수 있습니다. MySQL에 지금이 있습니까? 요즘 React를합니다 : P


3
이 솔루션이 정말 마음에 듭니다. 그러나 기본 테이블에 기본 키가 없거나 기본 키가 무엇인지 모르는 경우 약간 까다 롭습니다.
— Benjamin Eckstein 2014 년

1
최근에이 솔루션을 프로젝트에 사용하는 데 문제가 발생했습니다. 원래 테이블의 모든 인덱스가 기록 테이블에 복사되는 방식 때문입니다 (CREATE TABLE ... LIKE .... 작동 방식으로 인해). 히스토리 테이블에 고유 인덱스가 있으면 AFTER UPDATE 트리거의 INSERT 쿼리가 barf가 될 수 있으므로 제거해야합니다. 이 작업을 수행하는 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;
— 에릭 헤이즈

4
@transientclosure 원래 쿼리의 일부가 아닌 다른 필드를 기록에 가져 오는 방법을 제안 하시겠습니까? 예를 들어 누가 이러한 변경을 수행하는지 추적하고 싶습니다. 삽입을 위해 이미 owner필드가 있고 업데이트를 위해 updatedby필드를 추가 할 수 있지만 삭제를 위해 트리거를 통해 어떻게 할 수 있는지 잘 모르겠습니다. data_history사용자 ID로 행을 업데이트하는 것은 더럽습니다 .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;
$$

또 다른 해결책은 Revision 필드를 유지하고 저장할 때이 필드를 업데이트하는 것입니다. 최대 값이 최신 개정판인지 또는 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

사용자의 현재 주소를 찾기 위해 DESC 및 LIMIT 1이 수정 된 UserData를 검색합니다.

특정 기간 동안 사용자의 주소를 얻으려면 created_on bewteen (date1, date 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 (사용자 ID)를 id`하고 revision.
— Gajus

6

MariaDB는 사용자가 원하는 작업을 정확히 수행하는 표준 SQL 기능인 10.3 이후의 시스템 버전 관리를 지원합니다. 테이블 레코드 기록을 저장하고 SELECT쿼리 를 통해 액세스를 제공 합니다. MariaDB는 MySQL의 개방형 개발 포크입니다. 다음 링크를 통해 시스템 버전 관리에 대해 자세히 알아볼 수 있습니다.

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


위의 링크에서 다음을 참고하십시오. "mysqldump는 버전이 지정된 테이블에서 기록 행을 읽지 않으므로 기록 데이터가 백업되지 않습니다. 또한 타임 스탬프를 삽입 /으로 정의 할 수 없기 때문에 복원 할 수 없습니다. 사용자. "
— Daniel

4

단순히 bin 로그 파일을 사용하지 않는 이유는 무엇입니까? 복제가 Mysql 서버에 설정되고 binlog 파일 형식이 ROW로 설정되어 있으면 모든 변경 사항을 캡처 할 수 있습니다.

noplay라는 좋은 파이썬 라이브러리를 사용할 수 있습니다. 여기에 더 많은 정보가 있습니다 .


2
Binlog는 복제가 필요하지 않거나 필요하지 않은 경우에도 사용할 수 있습니다. Binlog에는 많은 유용한 사용 사례가 있습니다. 복제는 아마도 가장 일반적인 사용 사례 일 수 있지만 여기에서 언급 한 것처럼 백업 및 감사 기록에도 활용할 수 있습니다.
— webaholik

3

내 2 센트. 과도 솔루션과 매우 유사한 변경 사항을 정확히 기록하는 솔루션을 만들 것입니다.

내 ChangesTable은 간단합니다.

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

1) 주 테이블에서 전체 행이 변경 될 때 많은 항목이이 테이블에 들어갑니다. 그러나 가능성은 매우 낮으므로 큰 문제는 아닙니다 (일반적으로 사람들은 한 가지만 변경합니다). 2) OldVaue (및 NewValue 원하는) 모든 데이터가 될 수 있기 때문에 일종의 서사시적인 "모든 유형"이어야합니다. RAW 유형으로이를 수행하거나 JSON 문자열을 사용하여 안팎으로 변환하는 방법이있을 수 있습니다.

최소 데이터 사용량, 필요한 모든 것을 저장하고 한 번에 모든 테이블에 사용할 수 있습니다. 나는 지금 이것을 직접 조사하고 있지만 이것이 내가가는 길이 될 수 있습니다.

만들기 및 삭제의 경우 행 ID 만 있으면되고 필드는 필요하지 않습니다. 삭제시 기본 테이블 (활성?)에 플래그를 지정하는 것이 좋습니다.


0

이를 수행하는 직접적인 방법은 테이블에 트리거를 만드는 것입니다. 일부 조건 또는 매핑 방법을 설정합니다. 업데이트 또는 삭제가 발생하면 자동으로 '변경'테이블에 삽입됩니다.

그러나 가장 큰 부분은 많은 열과 많은 테이블이 있다면 어떨까요? 모든 테이블의 모든 열 이름을 입력해야합니다. 분명히 시간 낭비입니다.

이를 더 멋지게 처리하기 위해 열 이름을 검색하는 몇 가지 프로 시저 또는 함수를 만들 수 있습니다.

이 작업을 수행하기 위해 세 번째 부분 도구를 사용할 수도 있습니다. 여기에 Java 프로그램 Mysql Tracker를 작성합니다.


Mysql Tracker를 어떻게 사용할 수 있습니까?
— webchun

1
1. 각 테이블에 기본 키로 id 열이 있는지 확인하십시오. 2. java 파일을 로컬 (또는 IDE)에 복사합니다. 3. libs를 가져오고 데이터베이스 구성 및 구조에 따라 9-15 행에서 정적 변수를 편집합니다. 4. 구문 분석 및 자바 파일 5. 복사 콘솔 로그를 실행하고 MySQL의 명령으로 실행
— goforu

create table like table내가 쉽게 복제 모든 열을 생각한다
— 조나단
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.