23 điểm bởi GN⁺ 2026-01-23 | 4 bình luận | Chia sẻ qua WhatsApp
  • 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 triggerlự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

 
love7peace 2026-01-23

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ứ

 
nemorize 2026-01-24

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.

 
cherrycoder 2026-01-23

Đú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.

 
GN⁺ 2026-01-23
Ý 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_at thì 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ệu
    Trong đ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

    • Bạn nói đúng, nhưng tôi nghĩ điều đó chỉ đúng khi việc xóa là hiếm
      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 Postgres, bạn cũng có thể cấu hình để chỉ lập chỉ mục cho dữ liệu chưa bị soft delete
      Trong đa số trường hợp thì không cần, nhưng có thể giúp tiết kiệm RAM
    • Ở ngân hàng, soft delete chỉ là cách vá tạm để che đi việc thiếu khả năng kiểm toán (auditability)
      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)
    • Nếu muốn vận hành DB theo cấu trúc bất biến, có thể cân nhắc hệ thống như Datomic
      Với tính năng time travel, có thể truy vấn đầy đủ trạng thái trong quá khứ
    • Khi tôi làm ở một công ty bảo hiểm trước đây, mỗi bảng cũng được vận hành như append-only log
      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 filter
    Có 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

    • View là một công cụ đủ mạnh
      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

    • Thực tế, các hệ thống như Iceberg, Delta Lake, BigQuery đều cung cấp tính năng time travel
      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ô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

    • Xóa hoàn toàn thực chất là tự giới hạn khả năng phân tích dữ liệu của chính mình trong tương lai
      Lưu trữ thì rẻ, nên đừng bao giờ xóa dữ liệu
    • Nhưng điều thú vị là giáo sư lại không nói gì về việc chỉnh sử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

    • Nhưng nếu dữ liệu là tài sản có rủi ro rò rỉ, thì ngược lại có thể cần xóa hàng loạt
      Cần cân nhắc chi phí duy trì và rủi ro bảo mật
    • Nếu DB không phải bất biến, thì bản thân việc sửa đổi cũng đã tạo ra sự thật bị 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
    • Cá nhân tôi nghĩ kho dữ liệu chỉ nên hỗ trợ hai thao tác là truy vấn và chè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

    • Với các dự án đơn giản, thay vì thay đổi DB thì audit chính các API call lại hiệu quả hơn
  • 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

    • Dùng RLS (Row Level Security) của Postgres cũng có thể tự động ẩn các hàng đã soft delete
      Ứ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 đề

    • Theo kinh nghiệm của tôi, các đối tượng đã archive gần như rất ít khi được truy cập
      Đ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