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.

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:

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ề
- 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 đó.
- 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ụ.
- 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.