Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng một buổi sáng thứ Hai, phòng vận hành của một ngân hàng thương mại nhận được công văn từ Ngân hàng Nhà nước (SBV) yêu cầu giải trình: tại sao hạn mức tín dụng của một khách hàng doanh nghiệp đã bị nâng từ 5 tỷ lên 20 tỷ đồng vào lúc 23 giờ 47 phút ngày 14 tháng trước. Câu hỏi nghe có vẻ đơn giản, nhưng nếu hệ thống của bạn chỉ lưu trạng thái hiện tại — tức là cột credit_limit = 20.000.000.000 — thì bạn không thể trả lời được. Bạn không biết giá trị cũ là bao nhiêu, ai đã thay đổi, vào lúc nào, và từ trạng thái nào sang trạng thái nào.
Đây chính là lý do tồn tại của audit tables (bảng kiểm toán/nhật ký) và temporal data (dữ liệu theo thời gian). Trong các bài trước, chúng ta đã học cách mô hình hóa entity, attribute, key và relationship để biểu diễn thế giới hiện tại. Nhưng thế giới thực không đứng yên — dữ liệu thay đổi liên tục, và với rất nhiều hệ thống nghiệp vụ, lịch sử của sự thay đổi cũng quan trọng không kém bản thân dữ liệu.
Là một Business Analyst làm việc với data model, bạn cần biết khi nào phải đặt câu hỏi "Chúng ta có cần lưu lại lịch sử của entity này không?" và biết cách thiết kế cấu trúc để trả lời bốn nhu cầu kinh điển sau:
- Tuân thủ (Compliance): Các khung pháp lý như PDPL (Luật Bảo vệ Dữ liệu Cá nhân của Việt Nam), quy định của SBV, hay chuẩn SOX quốc tế đều yêu cầu phải truy vết được ai đã làm gì với dữ liệu nhạy cảm.
- Gỡ lỗi (Debugging): Khi nghiệp vụ hỏi "Cái này thay đổi lúc nào vậy?", audit log là nguồn sự thật duy nhất.
- Đối soát (Reconciliation): So khớp số liệu giữa các hệ thống, giữa các kỳ báo cáo, đòi hỏi biết được trạng thái dữ liệu tại một thời điểm cụ thể.
- Điều tra pháp y (Forensics): Khi xảy ra sự cố bảo mật hoặc gian lận, dấu vết thay đổi dữ liệu là bằng chứng quyết định.
Khái niệm cốt lõi
Phân biệt audit data và temporal data
Hai khái niệm này thường bị gộp làm một, nhưng chúng phục vụ mục đích khác nhau:
- Audit data trả lời câu hỏi "Ai đã thay đổi gì, khi nào, và bằng thao tác gì?" Nó tập trung vào hành động và trách nhiệm giải trình. Audit log thường ghi lại cả thao tác INSERT, UPDATE, DELETE, kèm theo người thực hiện và bối cảnh kỹ thuật (IP, phiên đăng nhập).
- Temporal data trả lời câu hỏi "Giá trị của dữ liệu này tại thời điểm X là gì?" Nó tập trung vào việc biểu diễn dữ liệu theo trục thời gian, cho phép bạn "tua ngược" để xem trạng thái quá khứ.
Hai loại thời gian: valid time và transaction time
Đây là khái niệm nền tảng của temporal modeling mà nhiều BA bỏ qua:
- Transaction time (thời gian giao dịch): thời điểm dữ liệu được ghi vào hệ thống. Ví dụ: hệ thống nhận được thông tin cập nhật lúc 10 giờ sáng.
- Valid time (thời gian hiệu lực): thời điểm dữ liệu có hiệu lực trong thực tế nghiệp vụ. Ví dụ: mức lương mới có hiệu lực từ ngày 1 tháng sau, dù được nhập vào hệ thống hôm nay.
Các mẫu thiết kế audit phổ biến
Mẫu 1 — Audit columns (cột kiểm toán nhúng): Thêm vào mỗi bảng nghiệp vụ các cột created_at, created_by, updated_at, updated_by. Đơn giản, nhẹ, nhưng chỉ ghi được lần thay đổi gần nhất — không có lịch sử đầy đủ.
Mẫu 2 — Shadow/History table (bảng lịch sử song song): Với mỗi bảng customer, tạo thêm bảng customer_history có cấu trúc tương tự cộng thêm cột operation (INSERT/UPDATE/DELETE), changed_at, changed_by. Mỗi lần bản ghi gốc thay đổi, một bản sao toàn bộ trạng thái cũ được ghi vào bảng lịch sử (thường qua trigger). Đây là mẫu cân bằng tốt nhất cho đa số hệ thống nghiệp vụ.
Mẫu 3 — Generic audit log (nhật ký kiểm toán tổng quát): Một bảng audit_log duy nhất cho toàn hệ thống, lưu kiểu key-value: table_name, record_id, column_name, old_value, new_value, changed_by, changed_at. Rất linh hoạt, dễ mở rộng cho bảng mới, nhưng truy vấn khó và dữ liệu phình to nhanh.
Mẫu 4 — Effective dating (versioning theo khoảng hiệu lực): Thay vì cập nhật bản ghi, mỗi thay đổi tạo một dòng mới với cặp cột valid_from và valid_to. Bản ghi đang hiệu lực có valid_to = NULL (hoặc một ngày xa trong tương lai như 9999-12-31). Đây chính là cách triển khai temporal data phổ biến nhất, tương đương Slowly Changing Dimension Type 2 trong data warehouse.
Soft delete — khi xóa không thực sự là xóa
Một mẫu liên quan mật thiết: thay vì DELETE bản ghi, ta thêm cột deleted_at (hoặc is_active). Bản ghi "bị xóa" vẫn nằm trong bảng để phục vụ audit và khôi phục, chỉ bị ẩn khỏi truy vấn nghiệp vụ thông thường. Điều này đặc biệt quan trọng với PDPL: bạn cần chứng minh được khi nào một bản ghi cá nhân bị xóa và ai yêu cầu.
Tình huống thực tế
Tình huống 1 — Ngân hàng TMCP: audit hạn mức tín dụng theo yêu cầu SBV
Một ngân hàng TMCP cỡ trung tại TP.HCM triển khai hệ thống phê duyệt tín dụng. Ban đầu, bảng credit_facility chỉ lưu trạng thái hiện tại với cột approved_limit. Khi SBV thanh tra và yêu cầu giải trình lịch sử điều chỉnh hạn mức của 200 khách hàng doanh nghiệp trong 12 tháng, đội ngũ vận hành choáng váng vì không có dữ liệu lịch sử.
Sau sự cố, BA thiết kế lại với mẫu shadow table kết hợp effective dating. Bảng credit_facility_history lưu mỗi phiên bản hạn mức với valid_from, valid_to, changed_by, approval_reference (số tờ trình phê duyệt), và change_reason. Một bản ghi điển hình:
| facility_id | approved_limit | valid_from | valid_to | changed_by | approval_ref |
|---|---|---|---|---|---|
| F-1024 | 5.000.000.000 | 2025-01-10 | 2025-06-14 | user_an | TT-2025-0042 |
| F-1024 | 20.000.000.000 | 2025-06-14 | NULL | user_binh | TT-2025-1187 |
Bài học: Với dữ liệu chịu quy định pháp lý, audit không phải là tính năng "nice-to-have" mà phải nằm trong data model ngay từ ngày đầu. Sửa sau khi đã mất lịch sử là không thể cứu vãn.
Tình huống 2 — Sàn TMĐT: bi-temporal cho giá sản phẩm
Một sàn thương mại điện tử kiểu Tiki/Shopee chạy hàng nghìn chương trình khuyến mãi. Đội marketing lên lịch giá mới trước khi nó có hiệu lực: ngày 25/11 họ nhập giá Black Friday sẽ áp dụng từ 0 giờ ngày 29/11. Hệ thống ban đầu chỉ có cột price đơn lẻ, khiến mỗi đợt sale phải có người trực canh đúng nửa đêm để bấm cập nhật — vừa rủi ro vừa mệt mỏi.
BA đề xuất mô hình bi-temporal cho bảng product_price:
valid_from,valid_to: khoảng thời gian giá có hiệu lực bán hàng (valid time).recorded_at: thời điểm giá được nhập vào hệ thống (transaction time).
Diễn giải: Khi một khách hàng khiếu nại "tôi đặt lúc 23:58 ngày 28/11 mà bị tính giá Black Friday", đội CSKH dùng cặp valid time + transaction time để chứng minh chính xác giá nào đang hiệu lực tại giây đó.
Bài học: Tách bạch "khi nào dữ liệu vào hệ thống" và "khi nào dữ liệu có hiệu lực" giúp loại bỏ thao tác thủ công rủi ro cao và cho phép giải quyết tranh chấp dựa trên dữ liệu.
Tình huống 3 — Công ty bảo hiểm: forensics sau sự cố sửa quyền lợi
Một công ty bảo hiểm nhân thọ phát hiện một nhân viên nội bộ đã âm thầm sửa quyền lợi hợp đồng của vài khách hàng quen, rồi sửa lại như cũ sau khi rút tiền hoa hồng bất thường. Vì hệ thống chỉ có cột updated_at và updated_by (mẫu audit columns đơn giản), mỗi lần sửa lại ghi đè dấu vết cũ — nên trên bề mặt mọi thứ trông bình thường.
Sau vụ này, công ty chuyển sang generic audit log bất biến (append-only): mọi thao tác INSERT/UPDATE/DELETE trên bảng nhạy cảm đều ghi một dòng mới vào audit_log, kèm old_value, new_value, db_user, app_user, ip_address, session_id. Bảng này được cấp quyền chỉ ghi thêm, không ai — kể cả DBA — được phép UPDATE hay DELETE.
Diễn giải: Lần sau, mọi chuỗi sửa-rồi-sửa-lại đều để lại dấu vết đầy đủ với mốc thời gian từng mili-giây, đủ làm bằng chứng pháp lý.
Bài học: Audit log dùng cho forensics phải bất biến. Nếu kẻ gian có thể sửa được chính nhật ký, nhật ký đó vô giá trị. Tính append-only quan trọng ngang với việc có nhật ký.
Hướng dẫn từng bước
Khi đứng trước một entity và phân vân có cần audit/temporal hay không, hãy đi theo quy trình sau:
Bước 1 — Xác định nhu cầu lịch sử. Đặt ba câu hỏi với chủ nghiệp vụ: (1) Có quy định pháp lý nào yêu cầu lưu lịch sử entity này không? (2) Có ai từng hỏi "giá trị này trước đây là gì"? (3) Nếu dữ liệu này bị sửa sai/cố ý, hậu quả lớn đến đâu? Nếu một trong ba câu trả lời là "có/nghiêm trọng", entity cần audit.
Bước 2 — Chọn loại thời gian cần lưu. Chỉ cần biết ai sửa khi nào? Chỉ cần transaction time. Có chuyện "ngày hiệu lực khác ngày nhập" không? Cần thêm valid time (bi-temporal).
Bước 3 — Chọn mẫu thiết kế theo bảng quyết định nhanh:
- Chỉ cần dấu vết tối thiểu → audit columns.
- Cần lịch sử đầy đủ một vài bảng quan trọng → shadow/history table.
- Cần audit toàn hệ thống, nhiều bảng, ưu tiên forensics → generic audit log (append-only).
- Cần truy vấn trạng thái tại thời điểm bất kỳ → effective dating với
valid_from/valid_to.
request_id/session_id để truy ngược ngữ cảnh.Bước 5 — Quyết định cơ chế ghi. Trigger ở tầng database đảm bảo không thao tác nào lọt lưới, kể cả khi ai đó sửa trực tiếp qua SQL. Ghi ở tầng ứng dụng linh hoạt hơn nhưng dễ bị bỏ sót. Với compliance, ưu tiên trigger.
Bước 6 — Lập kế hoạch vòng đời dữ liệu. Audit data phình rất nhanh. Thống nhất với nghiệp vụ về thời hạn lưu trữ (ví dụ PDPL và SBV thường yêu cầu 5–10 năm), và chiến lược archiving (chuyển dữ liệu cũ sang bảng/lưu trữ lạnh) ngay từ khâu thiết kế.
Bước 7 — Đặt ràng buộc bảo vệ. Audit table phải được cấp quyền append-only và đưa vào tài liệu data dictionary (Bài 17) để cả đội hiểu rõ ý nghĩa và tính bất biến của nó.
Lỗi thường gặp & mẹo
Lỗi 1 — Coi audit là việc tính sau. Rất nhiều dự án ship trước, tính audit sau, để rồi mất sạch lịch sử của giai đoạn đầu — đúng giai đoạn dễ có lỗi và gian lận nhất. Mẹo: đưa câu hỏi "entity này có cần audit không" vào checklist review mọi bảng nghiệp vụ.
Lỗi 2 — Audit log có thể bị sửa. Nếu nhật ký không bất biến, nó vô dụng cho forensics. Mẹo: cấp quyền chỉ-INSERT cho bảng audit, tách schema/tài khoản riêng, và cân nhắc ký số (hash chaining) cho dữ liệu cực nhạy cảm.
Lỗi 3 — Nhầm giữa valid time và transaction time. Dùng updated_at (transaction time) để trả lời câu hỏi nghiệp vụ về hiệu lực (valid time) sẽ cho kết quả sai khi có nhập trước/nhập trễ. Mẹo: luôn hỏi rõ "thời gian này là lúc nhập hay lúc có hiệu lực".
Lỗi 4 — Quên xử lý timezone. Audit dùng changed_at kiểu local time gây thảm họa khi hệ thống chạy đa vùng. Mẹo: luôn lưu mốc thời gian audit ở UTC (chi tiết ở Bài 28).
Lỗi 5 — Lưu cả password/dữ liệu nhạy cảm vào audit old_value/new_value. Vô tình biến audit log thành điểm rò rỉ dữ liệu, vi phạm PDPL. Mẹo: lập danh sách cột nhạy cảm cần loại trừ hoặc che (mask) trước khi ghi audit.
Lỗi 6 — Để audit data phình không kiểm soát. Một bảng audit không có chiến lược archiving có thể chiếm 80% dung lượng database sau vài năm. Mẹo: phân vùng (partition) theo tháng/năm và lên lịch archiving ngay từ đầu.
Bài tập thực hành
Bài tập 1 — Phân loại nhu cầu. Cho danh sách các entity của một ứng dụng đặt phòng khách sạn: user_profile, room, booking, payment, room_price, login_session. Với mỗi entity, xác định: (a) có cần audit không và vì sao, (b) cần transaction time, valid time, hay cả hai, (c) mẫu thiết kế phù hợp nhất.
Bài tập 2 — Thiết kế shadow table. Cho bảng employee_salary(employee_id, salary, position, effective_date). Hãy thiết kế bảng lịch sử đầy đủ cho phép trả lời câu hỏi: "Lương của nhân viên E-077 vào ngày 15/03/2025 là bao nhiêu?" Liệt kê đầy đủ các cột bạn thêm vào và giải thích vai trò từng cột.
Bài tập 3 — Bi-temporal. Một công ty bảo hiểm nhập ngày 10/06 rằng quyền lợi của hợp đồng tăng, có hiệu lực hồi tố từ 01/05. Sau đó ngày 20/06 phát hiện nhập sai, sửa lại con số. Hãy vẽ các dòng dữ liệu trong bảng policy_benefit với cả valid time và transaction time, sao cho có thể trả lời: "Vào ngày 15/06, hệ thống tin rằng quyền lợi hiệu lực ngày 05/05 là bao nhiêu?"
Bài tập 4 — Phát hiện rủi ro. Cho thiết kế audit chỉ có hai cột updated_at và updated_by trên bảng giao dịch tài chính. Liệt kê ít nhất ba câu hỏi forensics mà thiết kế này KHÔNG thể trả lời, và đề xuất cải tiến.
Tóm tắt
Audit và temporal data là cách data model nắm bắt lịch sử chứ không chỉ hiện tại. Bốn động lực chính là tuân thủ (PDPL, SBV, SOX), gỡ lỗi, đối soát và điều tra pháp y — và là BA, bạn cần đặt câu hỏi về nhu cầu lịch sử cho mọi entity quan trọng ngay từ khâu thiết kế.
Hãy ghi nhớ những điểm cốt lõi: phân biệt audit data (ai-làm-gì-khi-nào) với temporal data (giá trị tại thời điểm X); phân biệt transaction time (lúc nhập) với valid time (lúc hiệu lực), và biết khi nào cần mô hình bi-temporal. Chọn mẫu phù hợp — audit columns, shadow table, generic audit log, hay effective dating — theo đúng nhu cầu thay vì áp dụng máy móc. Quan trọng nhất: audit log phục vụ forensics phải bất biến và append-only, phải lưu thời gian ở UTC, phải tránh rò rỉ dữ liệu nhạy cảm, và phải có chiến lược lưu trữ vòng đời ngay từ đầu. Lịch sử dữ liệu một khi đã mất thì không thể tái tạo — hãy thiết kế để giữ nó từ ngày đầu tiên.