- Xóa mềm dựa trên cột
archived_at thường được dùng để khôi phục dữ liệu và tuân thủ quy định, nhưng theo thời gian sẽ làm tăng độ phức tạp và sự kém hiệu quả
- Cách này làm cho truy vấn, chỉ mục, migration và logic khôi phục trở nên phức tạp hơn, và vì phần lớn dữ liệu lưu trữ hầu như không được đọc lại nên gây ra tải không cần thiết cho cơ sở dữ liệu
- Các phương án thay thế được đề xuất gồm lưu trữ dựa trên sự kiện ở tầng ứng dụng, lưu trữ dựa trên trigger, và lưu trữ dựa trên WAL (Change Data Capture)
- Mỗi cách tiếp cận khác nhau về độ phức tạp vận hành, yêu cầu hạ tầng và mức độ dễ khôi phục; đặc biệt cách dựa trên WAL cần tích hợp với các hệ thống bên ngoài như Kafka
- Với dự án mới, cách tiếp cận dựa trên trigger là lựa chọn cân bằng nhất về tính đơn giản và khả năng bảo trì
Những vấn đề của xóa mềm
- Thông thường dữ liệu được xóa logic bằng cột boolean
deleted hoặc cột timestamp archived_at
- Có thể khôi phục khi khách hàng vô tình xóa dữ liệu
- Cũng có trường hợp cần lưu giữ vì mục đích tuân thủ hoặc kiểm toán
- Tuy nhiên, cột
archived_at gây ra sự phức tạp trên toàn bộ truy vấn, vận hành và mã ứng dụng
- Phần lớn dữ liệu lưu trữ không bao giờ được đọc lại
- Do vấn đề hành vi của API hoặc công cụ tự động hóa như Terraform, hàng triệu dòng không cần thiết có thể tích tụ
- Nếu không thiết lập công việc dọn dẹp dữ liệu lưu trữ, hiệu năng khi sao lưu và khôi phục cơ sở dữ liệu sẽ bị suy giảm
- Cần lọc dữ liệu lưu trữ trong truy vấn và chỉ mục, đồng thời tồn tại rủi ro rò rỉ dữ liệu
- Khi migration, việc xử lý dữ liệu cũ hoặc sửa giá trị mặc định trở nên khó khăn
- Logic khôi phục trở nên phức tạp hơn, và nếu cần gọi hệ thống bên ngoài thì có thể phát sinh lỗi
- Kết quả là cách dùng
archived_at trông có vẻ đơn giản nhưng chi phí bảo trì dài hạn cao
Lưu trữ ở tầng ứng dụng
- Khi xóa, hệ thống phát hành một sự kiện, gửi nó tới SQS để dịch vụ khác lưu trữ trên S3
- Ưu điểm
- Đơn giản hóa cơ sở dữ liệu chính và mã ứng dụng
- Dọn dẹp tài nguyên bên ngoài theo kiểu xử lý bất đồng bộ, giúp cải thiện hiệu năng và độ ổn định
- Có thể tuần tự hóa dưới dạng JSON để lưu trữ theo cấu trúc thân thiện với ứng dụng
- Nhược điểm
- Lỗi trong mã ứng dụng có thể gây mất dữ liệu lưu trữ
- Làm tăng độ phức tạp hạ tầng vận hành như hàng đợi thông điệp
- Dữ liệu lưu trữ trên S3 cần công cụ tìm kiếm và khôi phục
Lưu trữ dựa trên trigger
- Trigger trước khi xóa sẽ sao chép dòng dữ liệu sang một bảng archive riêng dưới dạng JSON
- Ví dụ bảng:
archive(id, table_name, record_id, data, archived_at, caused_by_table, caused_by_id)
- Khi xóa bằng khóa ngoại (cascade), dùng biến session (
archive.cause_table, archive.cause_id) để theo dõi nguyên nhân xóa
- Có thể truy vấn bản ghi cha nào đã xóa dữ liệu con
- Ưu điểm
- Bảng live được giữ sạch sẽ, không cần cột
archived_at
- Việc dọn dẹp bảng lưu trữ (
WHERE archived_at < NOW() - INTERVAL '90 days') rất đơn giản
- Giữ được hiệu quả của truy vấn và chỉ mục, đồng thời đơn giản hóa migration
- Giảm kích thước backup
- Bảng lưu trữ có thể được quản lý bằng tablespace riêng hoặc partition theo thời gian
Lưu trữ dựa trên WAL (Change Data Capture)
- Đọc log WAL của PostgreSQL và stream các sự kiện xóa sang hệ thống bên ngoài
- Công cụ tiêu biểu: Debezium (tích hợp với Kafka)
- Ví dụ luồng:
PostgreSQL → Debezium → Kafka → Consumer → Archive Storage
- Các lựa chọn thay thế nhẹ hơn
- pgstream: gửi trực tiếp WAL tới webhook hoặc hàng đợi thông điệp
- wal2json: xuất WAL dưới dạng JSON
- pg_recvlogical: công cụ logical replication tích hợp sẵn của PostgreSQL
- Độ phức tạp vận hành
- Phương án dựa trên Kafka cần giám sát, xử lý sự cố và tinh chỉnh
- Nếu consumer bị chậm, file WAL sẽ tích tụ → nguy cơ thiếu dung lượng đĩa
- Có thể giới hạn bằng cấu hình
max_slot_wal_keep_size của PostgreSQL 13+
- Cần bắt buộc giám sát độ trễ replication slot và thiết lập cảnh báo
- Ưu điểm
- Có thể ghi nhận mọi thay đổi mà không cần sửa mã ứng dụng
- Có thể stream tới nhiều đích khác nhau (S3, data warehouse, chỉ mục tìm kiếm)
- Không tạo thêm tải cho cơ sở dữ liệu chính
- Nhược điểm
- Độ phức tạp vận hành và chi phí hạ tầng cao
- Khi consumer bị chậm có thể mất dữ liệu hoặc cần đồng bộ lại
- Khi schema thay đổi cần phối hợp giữa nguồn và consumer
Ý tưởng về replica không xử lý xóa
- Có đề xuất duy trì một replica PostgreSQL bỏ qua các truy vấn DELETE
- Có thể tích lũy và lưu giữ toàn bộ dữ liệu chưa bị xóa
- Có thể truy vấn trực tiếp dữ liệu lưu trữ
- Các vấn đề tiềm ẩn
- Có thể không phân biệt được thông tin đã bị xóa
- Có nguy cơ xung đột khi áp dụng migration
- Tăng chi phí lưu trữ và vận hành
Kết luận
- Trong các dự án mới, cách lưu trữ dựa trên trigger là lựa chọn thực tế nhất
- Thiết lập đơn giản và giữ cho bảng live luôn gọn gàng
- Dễ truy vấn và quản lý dữ liệu lưu trữ mà không cần hạ tầng riêng
- Nếu đã có sẵn hạ tầng phức tạp hoặc cần stream tới nhiều đích, thì cách tiếp cận dựa trên WAL sẽ phù hợp hơn
4 bình luận
Nếu dựa trên trigger thì tôi được biết là sẽ gây tải cho DB mà...? Lại còn khuyến nghị dùng trigger nữa chứ
Nếu mức tải phát sinh từ từng ấy trigger đã trở thành vấn đề thì ngay cả khi không có trigger, tình huống đó vốn dĩ cũng đã đầy rẫy vấn đề rồi.
Đúng là suy cho cùng, quy định lúc nào cũng là chi phí. Mà dù sao thì đó cũng là phần cuối cùng người tiêu dùng phải gánh thôi.
Ý kiến trên Hacker News
Trong lĩnh vực ngân hàng nơi tôi làm việc, tôi lại thấy soft delete có lợi hơn
Nếu có cột
deleted_atthì việc viết query rõ ràng hơn, và ở các query phân tích hay trang quản trị cũng có thể xử lý cùng một tập dữ liệuTrong đa số trường hợp, việc xóa là hiếm, và các hàng đã soft delete hầu như chưa từng gây ra vấn đề hiệu năng
Ngoài ra, do quan hệ vẫn được giữ nguyên nên khôi phục (undo) cũng đơn giản
Tôi còn thích đi xa hơn, là biến hàng thành bất biến (immutable) hoàn toàn và khi cập nhật thì thêm hàng mới
Nếu muốn lưu log, tôi nghĩ cách tốt là dùng DB trigger để ghi vào bảng sao chép khi INSERT/UPDATE/DELETE
Với những bảng tôi từng thấy mà 50~70% bản ghi đã bị soft delete thì suy giảm hiệu năng là rất rõ
Cuối cùng, soft delete còn tùy tình huống và cần phân tích trước
Trong đa số trường hợp thì không cần, nhưng có thể giúp tiết kiệm RAM
Giải pháp thực sự là Event Sourcing, tức phải ghi lại mọi thay đổi dưới dạng sự kiện
Hiệu năng sẽ giảm, nhưng có thể bù bằng snapshot và đồng bộ hóa (sync)
Với tính năng time travel, có thể truy vấn đầy đủ trạng thái trong quá khứ
Trạng thái mới nhất nằm ở hàng có timestamp lớn nhất, còn trạng thái quá khứ có thể xem bằng bộ lọc
Cách này cho phép quản lý lịch sử rất mạnh
Cái bẫy lớn nhất của soft delete là độ phức tạp của query
Ban đầu ai cũng nghĩ chỉ cần thêm
WHERE deleted_at IS NULL, nhưng vài tháng sau thì dữ liệu ma xuất hiện trong báo cáo vì thiếu filterCó thể giải quyết bằng View, nhưng rồi vẫn phải duy trì mẫu truy cập song song, và khi cần xem dữ liệu đã xóa thì phải vượt qua lớp trừu tượng đó
Event sourcing sạch sẽ hơn, nhưng vì gánh nặng vận hành lớn nên đa số chọn cách tiếp cận lai
Vấn đề là nhiều SWE và kỹ sư BI không quen với SQL và thiết kế schema
Vấn đề còn phổ biến hơn soft delete là xử lý Type 2 Slowly Changing Dimension
Đa số lại tạo audit table không cần thiết rồi lặp đi lặp lại các UPDATE/INSERT kém hiệu quả
Thực ra DB là một hệ thống rất đẹp, chỉ tiếc là nó không được tôn trọng đúng mức
Tôi nghĩ sẽ rất tốt nếu soft delete được cung cấp như một tính năng tích hợp của DB
Nếu có thể bật theo từng bảng và chọn chiến lược xử lý xóa thì sẽ rất lý tưởng
Nhưng vì nhiều đội có yêu cầu tùy biến nên cuối cùng vẫn triển khai theo kiểu SCD (Slowly Changing Dimension)
Theo kinh nghiệm của tôi, cách tiếp cận dựa trên trigger là ổn định nhất
Bảng archive nên được giữ ở dạng append-only, và việc khôi phục nên xử lý ở tầng ứng dụng
Update được xem như soft delete, và trigger sẽ chụp lại trạng thái trước đó
Trigger nhất định phải chạy ở thời điểm BEFORE, và logic cần đơn giản
Partition theo tháng là phổ biến, còn nếu tải ghi nhiều thì nên chia theo ngày
Tôi muốn DB tiến hóa từ stateful → stateless
Tôi thích cấu trúc ghi mọi thay đổi thành sự kiện append-only và biểu diễn dữ liệu cần thiết bằng view
Sẽ lý tưởng nếu DB tự động quản lý materialized index
Một số DB hiện đại đã có các tính năng như vậy, nhưng đến giờ vẫn còn thiếu phát triển theo hướng OLTP
Có thể tham khảo giải thích của Martin Fowler
Ở công ty tôi từng làm trước đây, họ áp dụng soft delete cho mọi hệ thống
Tôi vẫn nhớ giáo sư từng nói: “Trong thế giới kinh doanh, dữ liệu không bao giờ bị xóa”
Lưu trữ thì rẻ, nên đừng bao giờ xóa dữ liệu
Cơ sở dữ liệu là nơi lưu trữ sự thật (fact)
Việc tạo record là một sự thật mới, còn xóa cũng là một sự thật khác
Nhưng nếu xóa vật lý một hàng thì sự thật cũng biến mất
Trong đa số trường hợp, kiểu xóa như vậy là không mong muốn
Cần cân nhắc chi phí duy trì và rủi ro bảo mật
Quyết định lưu trữ dữ liệu vĩnh viễn cần được cân nhắc cẩn thận
Để làm được vậy, điều quan trọng là phải hiểu vòng đời của dữ liệu
Ở Firezone, ban đầu họ dùng soft delete cho audit log, nhưng đã bỏ vì vấn đề migration
Thay vào đó, họ chuyển sang dùng Postgres CDC (Change Data Capture) để xuất sự kiện sang một bảng riêng tối ưu cho ghi
Tôi nghĩ soft delete hữu ích cho tính năng khôi phục người dùng, nhưng không phù hợp cho kiểm toán hay tuân thủ quy định
Tạo View trên các bảng có trường soft delete để ẩn các hàng đã xóa là một cách khá gọn
Làm vậy thì ứng dụng không cần phải bận tâm đến việc bản ghi đã bị xóa hay chưa
Ứng dụng vẫn đọc/ghi/xóa trên cùng một bảng như cũ
Có người đặt câu hỏi phải xử lý schema drift thế nào
Nếu muốn khôi phục về sau dữ liệu đã được tuần tự hóa theo schema tại thời điểm xóa, thì thay đổi schema sẽ trở thành vấn đề
Đa số trường hợp khôi phục diễn ra trong vài ngày sau khi xóa nên ít bị ảnh hưởng bởi thay đổi schema
Việc migrate archive cũ sang model mới là công việc phức tạp và rất dễ lỗi
Cuối cùng, chiến lược tiếp cận sẽ khác nhau tùy theo cách hệ thống được sử dụng