MySQL 데이터베이스의 레코드 변경 사항을 추적 할 수 있는지 물었습니다. 따라서 필드가 변경되면 이전과 새 항목을 사용할 수 있으며이 날짜가 발생했습니다. 이를 수행하는 기능이나 일반적인 기술이 있습니까?
그렇다면 나는 이와 같은 일을 할 생각이었다. 라는 테이블을 만듭니다 changes
. 마스터 테이블 과 동일한 필드를 포함 하지만 이전 및 새 접두사가 붙지 만 실제로 변경된 필드와 해당 필드에만 해당됩니다 TIMESTAMP
. 인덱싱됩니다 ID
. 이렇게하면 SELECT
각 레코드의 기록을 표시 하는 보고서를 실행할 수 있습니다. 이것이 좋은 방법입니까? 감사!
답변
미묘합니다.
비즈니스 요구 사항이 “데이터 변경 사항을 감사하고 싶습니다-누가 언제 무엇을 했습니까?”인 경우 일반적으로 감사 테이블을 사용할 수 있습니다 (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이 필요하지 않지만 쿼리가 조금 더 쉬워집니다). 디자인과 데이터베이스 액세스가 복잡해집니다. 그러나 그것은 세상을 훨씬 더 쉽게 재구성 할 수있게합니다.
답변
이를 수행하는 간단한 방법은 다음과 같습니다.
먼저 추적하려는 각 데이터 테이블에 대한 기록 테이블을 만듭니다 (아래 쿼리 예). 이 테이블에는 데이터 테이블의 각 행에서 수행되는 각 삽입, 업데이트 및 삭제 쿼리에 대한 항목이 있습니다.
히스토리 테이블의 구조는 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
답변
이를 해결하기 위해 트리거를 만들 수 있습니다. 다음은이를위한 자습서입니다 (보관 된 링크).
데이터베이스에 제약 조건과 규칙을 설정하는 것이 동일한 작업을 처리하는 특수 코드를 작성하는 것보다 낫습니다. 다른 개발자가 모든 특수 코드를 우회하고 데이터베이스를 열악한 데이터 무결성으로 남겨 둘 수있는 다른 쿼리를 작성하는 것을 방지하기 때문입니다.
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이 가장 최근 행인지 결정할 수 있습니다. 그것은 당신에게 달려 있습니다.
답변
해결 방법은 다음과 같습니다.
사용자 테이블은 다음과 같습니다.
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)을 사용할 수 있습니다.
답변
MariaDB는 사용자가 원하는 작업을 정확히 수행하는 표준 SQL 기능인 10.3 이후의 시스템 버전 관리를 지원합니다. 테이블 레코드 기록을 저장하고 SELECT
쿼리 를 통해 액세스를 제공 합니다. MariaDB는 MySQL의 개방형 개발 포크입니다. 다음 링크를 통해 시스템 버전 관리에 대해 자세히 알아볼 수 있습니다.
답변
단순히 bin 로그 파일을 사용하지 않는 이유는 무엇입니까? 복제가 Mysql 서버에 설정되고 binlog 파일 형식이 ROW로 설정되어 있으면 모든 변경 사항을 캡처 할 수 있습니다.
답변
내 2 센트. 과도 솔루션과 매우 유사한 변경 사항을 정확히 기록하는 솔루션을 만들 것입니다.
내 ChangesTable은 간단합니다.
DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue
1) 주 테이블에서 전체 행이 변경 될 때 많은 항목이이 테이블에 들어갑니다. 그러나 가능성은 매우 낮으므로 큰 문제는 아닙니다 (일반적으로 사람들은 한 가지만 변경합니다). 2) OldVaue (및 NewValue 원하는) 모든 데이터가 될 수 있기 때문에 일종의 서사시적인 “모든 유형”이어야합니다. RAW 유형으로이를 수행하거나 JSON 문자열을 사용하여 안팎으로 변환하는 방법이있을 수 있습니다.
최소 데이터 사용량, 필요한 모든 것을 저장하고 한 번에 모든 테이블에 사용할 수 있습니다. 나는 지금 이것을 직접 조사하고 있지만 이것이 내가가는 길이 될 수 있습니다.
만들기 및 삭제의 경우 행 ID 만 있으면되고 필드는 필요하지 않습니다. 삭제시 기본 테이블 (활성?)에 플래그를 지정하는 것이 좋습니다.
