레코드 변경 내역을 추적하는 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=1id_user=1? 처음에 나는 당신의 계산이 있다고 생각 0,2,3,...했지만 id_user=2수정 계산에 대한 것을 보았습니다0,1, ...
Pathros

1
당신은 필요하지 않습니다 idid_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.