Có ba mức, chọn theo việc lịch sử là tính năng nghiệp vụ hay chỉ để đối soát.
1. Bảng audit log — bảng chung ghi mỗi lần thay đổi kèm diff.
create table audit_log (
id bigserial primary key,
table_name text not null,
record_id bigint not null,
action text not null, -- insert / update / delete
changes jsonb not null, -- {"price": {"old": 10, "new": 12}}
actor_id bigint,
created_at timestamptz not null default now()
);Rẻ, một bảng dùng cho mọi thực thể, ghi được bằng trigger nên không sót đường ghi nào. Nhược điểm: dựng lại trạng thái tại thời điểm T phải phát lại diff — không tiện làm tính năng người dùng thấy.
2. Bảng history song song (versioning theo hàng) — mỗi thực thể có bảng *_history cùng cột, thêm khoảng hiệu lực.
create table documents_history (
document_id bigint not null,
version int not null,
title text not null,
body text,
valid_range tstzrange not null,
actor_id bigint,
primary key (document_id, version)
);Truy vấn "bản ghi tại ngày X" chỉ là một điều kiện chứa trên valid_range — PostgreSQL còn chặn được khoảng thời gian chồng nhau bằng exclusion constraint. Đây là lựa chọn hợp lý cho tính năng "xem lịch sử / khôi phục bản cũ".
3. Event sourcing — không lưu trạng thái, chỉ lưu chuỗi sự kiện bất biến rồi tính trạng thái bằng cách phát lại. Lịch sử là đầy đủ nhất và audit là miễn phí, nhưng mọi truy vấn đều cần projection/read model, đổi schema sự kiện rất phức tạp. Chỉ chọn khi bản thân nghiệp vụ là dòng sự kiện (kế toán, sổ giao dịch).
Chọn thế nào: cần trả lời "ai sửa gì" cho thanh tra → audit log. Cần người dùng xem và khôi phục phiên bản → bảng history. Cần tái hiện toàn bộ quá trình và không được mất bất kỳ chuyển trạng thái nào → event sourcing. Kèm theo, cột version trên bảng chính còn dùng lại được cho optimistic locking.