PostgreSQL ghi mọi thay đổi vào WAL (Write-Ahead Log) trước khi áp vào bảng — đó là thứ đảm bảo dữ liệu không mất khi database crash. Nhưng WAL cũng là chi phí: mỗi lần ghi phải ghi hai lần (một vào WAL, một vào bảng). Với dữ liệu có thể dựng lại được — bảng staging của ETL, cache tính toán, kết quả trung gian — sự an toàn đó là thừa thãi. UNLOGGED TABLE cho phép bạn từ bỏ WAL để đổi lấy tốc độ. Bài này đo thật cả phần được lẫn phần mất, và chứng minh cái mất bằng một cú crash thật.

Bảng không ghi WAL

Cú pháp chỉ khác đúng một từ khoá:

CREATE UNLOGGED TABLE t_unlogged (id int, ten text, so int);

Mọi thao tác ghi trên bảng này không sinh WAL. PostgreSQL vẫn ghi dữ liệu vào các trang trên đĩa như thường, nhưng bỏ qua bước ghi log trước. Kết quả: ít I/O hơn, ít áp lực lên checkpoint, và — như ta sẽ thấy — nhanh hơn rõ rệt khi nạp khối lượng lớn.

Ảnh chụp đoạn mã SQL nền tối minh hoạ UNLOGGED TABLE bỏ ghi WAL để nạp nhanh đổi lại mất dữ liệu khi crash PostgreSQL 16 dữ liệu tạm bảng trung gian ETL cache có thể dựng lại, tạo bảng không ghi WAL chỉ thêm một từ khoá CREATE UNLOGGED TABLE t_unlogged id int ten text so int mọi thao tác ghi không sinh WAL nhanh hơn ít I/O hơn, cái giá crash dừng bẩn là bảng bị cắt sạch về rỗng không có WAL nên PostgreSQL không thể phục hồi bảng này sau crash recovery bảng unlogged bị TRUNCATE về 0 dòng khởi động lại sạch shutdown đúng cách thì dữ liệu vẫn còn, chuyển đổi hai chiều khi cần ALTER TABLE t SET LOGGED u sang p viết lại cả bảng vào WAL ALTER TABLE t SET UNLOGGED p sang u bỏ ghi WAL từ đây kiểm SELECT relpersistence FROM pg_class WHERE relname bằng t u unlogged p permanent bền, dùng đúng chỗ dữ liệu dựng lại được hợp bảng trung gian ETL staging nhập liệu cache tính toán dữ liệu phiên tạm kết quả trung gian của job không hợp dữ liệu nghiệp vụ thật đơn hàng giao dịch tiền lưu ý unlogged không được nhân bản sang replica streaming

Hình 1: CREATE UNLOGGED TABLE bỏ ghi WAL; đổi lại crash là bảng bị cắt sạch. ALTER TABLE ... SET LOGGED/UNLOGGED chuyển đổi hai chiều; kiểm bằng relpersistence trong pg_class.

Đo thật: 0 byte WAL và nhanh hơn

Nạp ba triệu dòng vào bảng thường (LOGGED) và bảng UNLOGGED, đo cả thời gian lẫn lượng WAL sinh ra:

Ảnh chụp bảng kết quả đo thật nền tối UNLOGGED vs LOGGED PostgreSQL 16 nạp 3000000 dòng, nạp và WAL sinh ra bảng LOGGED thường thời gian INSERT 905 ms WAL 230 MB bảng UNLOGGED 627 ms WAL 0 bytes unlogged nhanh khoảng 1,4 lần và sinh 0 byte WAL không ghi log ghi trước nên ít I/O ít áp lực checkpoint không đẩy dữ liệu sang replica, thử nghiệm crash SIGKILL dừng bẩn rồi khởi động lại relpersistence t_logged bằng p t_unlogged bằng u p permanent u unlogged log khởi động lại LOG redo done crash recovery chạy LOG checkpoint complete end-of-recovery bảng t_logged trước crash 3000000 sau crash 3000000 còn nguyên bảng t_unlogged trước crash 3000000 sau crash 0 bị cắt sạch, chuyển unlogged sang logged ALTER TABLE SET LOGGED 1 triệu dòng ALTER TABLE Time 208 ms WAL sinh ra 77 MB relpersistence p viết lại toàn bộ bảng vào WAL để từ nay dữ liệu bền qua crash

Hình 2: LOGGED nạp 905 ms sinh 230 MB WAL; UNLOGGED nạp 627 ms sinh 0 byte WAL. Sau crash (SIGKILL): bảng logged còn nguyên 3 triệu dòng, bảng unlogged bị cắt về 0. ALTER SET LOGGED 1 triệu dòng: 208 ms, 77 MB WAL.

Con số thật:

  • LOGGED: nạp mất 905 ms, sinh 230 MB WAL.
  • UNLOGGED: nạp mất 627 ms, sinh 0 byte WAL.

Bảng unlogged nhanh hơn ~1,4 lần và không sinh WAL nào cả. Lợi ích lớn nhất thực ra không chỉ là 1,4 lần trên câu INSERT này, mà là 230 MB WAL không phải ghi: trên hệ thống thật, WAL đó còn phải được checkpoint dọn, được sao lưu, và (nếu có replica) được đẩy qua mạng. Bảng unlogged cắt sạch toàn bộ chuỗi chi phí đó.

Chứng minh cái giá: một cú crash thật

Lời hứa "nhanh hơn" đi kèm một điều kiện nghiêm khắc, và tôi không muốn chỉ nói suông. Cả hai bảng đang có ba triệu dòng. Tôi gửi SIGKILL cho tiến trình PostgreSQL — mô phỏng đúng một cú crash (mất điện, OOM killer, kill -9) — rồi khởi động lại:

Log khởi động cho thấy redo done — PostgreSQL chạy crash recovery, phát lại WAL để khôi phục trạng thái nhất quán. Sau khi lên lại:

  • Bảng LOGGED: vẫn 3.000.000 dòng — còn nguyên. WAL đã cứu nó.
  • Bảng UNLOGGED: 0 dòng — bị cắt sạch.

Đây là hành vi cố ý, không phải lỗi. Vì bảng unlogged không có WAL, PostgreSQL không có cách nào biết trạng thái của nó trước lúc crash có nhất quán hay không, nên để an toàn nó TRUNCATE bảng về rỗng. Cột relpersistence trong pg_class cho biết loại bảng: p là permanent (bền), u là unlogged.

Lưu ý quan trọng: chỉ crash (dừng bẩn) mới cắt bảng. Nếu bạn tắt database đúng cách (pg_ctl stop, shutdown sạch), dữ liệu unlogged vẫn còn. Nhưng bạn không bao giờ được dựa vào điều đó cho dữ liệu quan trọng — crash là thứ xảy ra đúng lúc bạn không mong.

Chuyển đổi khi nhu cầu đổi

Bạn có thể đổi một bảng qua lại giữa hai chế độ mà không cần tạo lại:

ALTER TABLE t SET LOGGED;     -- unlogged -> permanent: viết lại cả bảng vào WAL
ALTER TABLE t SET UNLOGGED;   -- permanent -> unlogged: bỏ ghi WAL từ đây

Điều này mở ra một mẫu dùng rất hay: nạp dữ liệu vào bảng UNLOGGED (nhanh, không WAL), xử lý/làm sạch xong, rồi SET LOGGED để "chốt" nó thành dữ liệu bền. Đo thật: ALTER TABLE ... SET LOGGED trên một triệu dòng mất 208 ms và sinh 77 MB WAL — vì nó phải viết lại toàn bộ bảng vào WAL một lần để từ đó dữ liệu được bảo vệ. Sau lệnh, relpersistence chuyển từ u sang p.

Đánh đổi cần cân nhắc

Chỉ dùng cho dữ liệu dựng lại được. Đây là ranh giới tuyệt đối: bảng staging của ETL, cache có thể tính lại, dữ liệu phiên tạm, kết quả trung gian của một job — hợp lý. Đơn hàng, giao dịch tiền, bất cứ thứ gì không tái tạo được — tuyệt đối không. Một cú crash là mất trắng, không cảnh báo.

Unlogged không được nhân bản. Vì replica streaming dựng lại từ chính WAL, bảng unlogged (không sinh WAL) không xuất hiện trên replica. Nếu bạn đọc từ replica, bảng unlogged sẽ rỗng ở đó. Đây là điểm hay quên khi hệ thống có standby.

So với TEMP TABLE. Bảng TEMPORARY cũng không ghi WAL và cũng nhanh, nhưng nó chỉ sống trong một phiên và tự biến mất khi phiên kết thúc. Bảng UNLOGGED sống lâu dài (qua các phiên, qua restart sạch) và chia sẻ được giữa nhiều kết nối — đó là khác biệt chính khi chọn giữa hai loại. Cần bảng tạm dùng chung nhiều phiên thì UNLOGGED; cần bảng nháp riêng một phiên thì TEMP.

Ba ý mang về

  1. UNLOGGED TABLE bỏ ghi WAL nên nạp nhanh hơn và không tốn I/O ghi log: đo thật, nạp 3 triệu dòng sinh 0 byte WAL so với 230 MB của bảng thường, nhanh ~1,4 lần — và cắt luôn chi phí checkpoint, sao lưu, đẩy replica cho lượng WAL đó.
  2. Cái giá là mất sạch dữ liệu khi crash: chứng minh bằng SIGKILL thật — sau crash recovery, bảng logged còn nguyên 3 triệu dòng còn bảng unlogged bị TRUNCATE về 0; chỉ dùng cho dữ liệu dựng lại được (staging ETL, cache), không bao giờ cho dữ liệu nghiệp vụ.
  3. Chuyển đổi được hai chiều bằng ALTER TABLE SET LOGGED/UNLOGGED (mẫu hay: nạp nhanh dạng unlogged rồi chốt thành logged — đo thật 208 ms + 77 MB WAL cho 1 triệu dòng); nhớ rằng unlogged không xuất hiện trên replica.

Phần sau ta so hai cách làm rỗng bảng thường bị coi là như nhau: Phần sau đo TRUNCATE vs DELETE — vì sao TRUNCATE gần như tức thời còn DELETE phải quét từng dòng, và những khác biệt tinh vi về khoá, trigger và giao dịch.