Bạn cần làm rỗng một bảng. DELETE FROM bang và TRUNCATE bang cùng cho ra một bảng trống, nên nhìn qua chúng như nhau. Thực ra chúng khác nhau tận gốc về cách vận hành: DELETE đi qua từng dòng đánh dấu là chết, còn TRUNCATE chỉ vứt cả tệp dữ liệu rồi tạo tệp mới rỗng. Khác biệt đó dẫn tới chênh lệch hiệu năng hàng chục lần, và một loạt hành vi tinh vi về giao dịch, sequence và trigger mà nếu không biết sẽ vấp. Bài này đo thật tất cả.

Vì sao TRUNCATE gần như tức thời

DELETE là thao tác DML bình thường theo MVCC: nó quét bảng, đánh dấu mỗi dòng là dead tuple, ghi WAL cho từng dòng để có thể rollback và phục hồi sau crash. Với ba triệu dòng, đó là ba triệu lần đánh dấu và một lượng WAL khổng lồ. Tệ hơn, những dòng chết đó vẫn chiếm chỗ cho tới khi VACUUM dọn — và ngay cả VACUUM thường cũng không trả tệp về hệ điều hành, phải VACUUM FULL mới thu nhỏ được.

TRUNCATE thì khác hẳn: nó là thao tác DDL, chỉ đơn giản bỏ tệp dữ liệu hiện tại của bảng và tạo một tệp rỗng mới. Không quét dòng nào, không sinh dead tuple, và chỗ trên đĩa được trả lại ngay lập tức.

Ảnh chụp đoạn mã SQL nền tối minh hoạ TRUNCATE vs DELETE hai cách làm rỗng bảng khác nhau tận gốc PostgreSQL 16 DELETE quét từng dòng TRUNCATE bỏ nguyên tệp dữ liệu, DELETE đánh dấu từng dòng là chết không trả lại chỗ DELETE FROM bang quét đánh dấu mỗi dòng dead tuple sinh WAL cho từng dòng bảng không nhỏ lại cần VACUUM FULL kích hoạt trigger ON DELETE cho mỗi dòng có WHERE xoá chọn lọc, TRUNCATE vứt cả tệp dữ liệu trả chỗ ngay cho hệ điều hành TRUNCATE bang tức thời không quét dòng nào TRUNCATE bang RESTART IDENTITY reset luôn sequence về 1 TRUNCATE a b c nhiều bảng một lệnh TRUNCATE bang CASCADE xoá luôn bảng tham chiếu FK, TRUNCATE vẫn có giao dịch trong PostgreSQL khác MySQL BEGIN TRUNCATE bang ROLLBACK dữ liệu quay lại nguyên vẹn TRUNCATE không tự commit, cái giá của TRUNCATE khoá ACCESS EXCLUSIVE chặn cả đọc lẫn ghi tới khi xong nhưng xong rất nhanh DELETE chỉ khoá dòng ROW EXCLUSIVE SELECT khác vẫn chạy TRUNCATE là all-or-nothing không có WHERE không xoá chọn lọc

Hình 1: DELETE đánh dấu từng dòng (dead tuple, WAL, trigger cấp dòng); TRUNCATE vứt cả tệp dữ liệu (tức thời, trả chỗ ngay). TRUNCATE vẫn có giao dịch nhưng lấy khoá ACCESS EXCLUSIVE và là all-or-nothing.

Đo thật: 678 ms so với 20 ms

Bảng ba triệu dòng, 331 MB. Làm rỗng cả bảng bằng hai cách:

Ảnh chụp bảng kết quả đo thật nền tối TRUNCATE vs DELETE PostgreSQL 16 bảng 3000000 dòng 331 MB, làm rỗng cả bảng DELETE FROM bang thời gian 678 ms WAL 413 MB dead tuple 3000000 cỡ bảng sau 331 MB không đổi, TRUNCATE bang thời gian 19,85 ms WAL 21 kB dead tuple 0 cỡ bảng sau 16 kB trả về HĐH, TRUNCATE nhanh khoảng 34 lần WAL bé tí và trả chỗ ngay DELETE để lại 3 triệu dead tuple và bảng vẫn 331 MB phải VACUUM FULL mới nhỏ lại, 1 TRUNCATE có giao dịch ROLLBACK khôi phục được khác MySQL BEGIN TRUNCATE bang_y ROLLBACK số dòng 1000 dữ liệu quay lại, 2 RESTART IDENTITY reset sequence hay không DELETE rồi INSERT id mới bằng 1001 sequence không reset TRUNCATE RESTART IDENTITY id mới bằng 1 sequence reset về 1, 3 trigger BEFORE DELETE FOR EACH ROW trên 5 dòng DELETE 5 chạy mỗi dòng TRUNCATE 0 trigger dòng không chạy TRUNCATE có riêng trigger cấp lệnh FOR EACH STATEMENT không phải cấp dòng

Hình 2: DELETE cả bảng 678 ms, 413 MB WAL, 3 triệu dead tuple, bảng vẫn 331 MB. TRUNCATE 19,85 ms, 21 kB WAL, 0 dead tuple, bảng còn 16 kB. Cùng khác biệt về giao dịch, sequence và trigger.

Con số thật:

  • DELETE: 678 ms, sinh 413 MB WAL, để lại 3 triệu dead tuple, và bảng vẫn 331 MB — chỗ không được trả lại.
  • TRUNCATE: 19,85 ms — nhanh hơn ~34 lần — sinh vỏn vẹn 21 kB WAL, 0 dead tuple, và bảng thu về 16 kB (chỗ trả ngay cho hệ điều hành).

Khác biệt không chỉ ở tốc độ mà ở hệ quả sau đó: sau DELETE, bạn còn ba triệu dead tuple cần VACUUM dọn và một tệp 331 MB phình rỗng cần VACUUM FULL mới thu nhỏ. Sau TRUNCATE, mọi thứ đã sạch. Nếu ý định là xoá sạch cả bảng, TRUNCATE gần như luôn là lựa chọn đúng.

Ba khác biệt hành vi dễ vấp

1. TRUNCATE vẫn có giao dịch trong PostgreSQL. Đây là điểm khác nhiều CSDL khác (ví dụ MySQL, nơi TRUNCATE tự commit ngầm). Trong PostgreSQL, TRUNCATE nằm trong giao dịch như mọi lệnh khác — bạn có thể ROLLBACK nó:

BEGIN;
  TRUNCATE bang_y;
ROLLBACK;   -- dữ liệu quay lại nguyên vẹn

Đo thật: sau BEGIN; TRUNCATE; ROLLBACK, bảng vẫn còn đủ 1.000 dòng. Điều này khiến TRUNCATE an toàn hơn nhiều người tưởng — lỡ tay trong một giao dịch vẫn cứu được.

2. RESTART IDENTITY reset sequence, DELETE thì không. Sau khi DELETE mọi dòng rồi INSERT dòng mới, id tiếp tục từ chỗ cũ (đo thật: 1001) — vì sequence không bị đụng tới. TRUNCATE ... RESTART IDENTITY reset sequence về đầu, nên dòng mới có id = 1. Nếu bạn muốn "bắt đầu lại từ số một", phải nêu rõ RESTART IDENTITY.

3. Trigger cấp dòng không chạy với TRUNCATE. Trigger BEFORE/AFTER DELETE ... FOR EACH ROW chạy một lần cho mỗi dòng bị DELETE (đo thật: xoá 5 dòng → trigger chạy 5 lần). TRUNCATE không kích hoạt trigger cấp dòng nào (0 lần) — vì nó không đụng tới từng dòng. Nếu logic của bạn dựa vào trigger ON DELETE (ví dụ ghi log kiểm toán, dọn bảng liên quan), TRUNCATE sẽ bỏ qua chúng. PostgreSQL có riêng loại trigger FOR EACH STATEMENT ... ON TRUNCATE cho trường hợp này, nhưng bạn phải khai riêng.

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

TRUNCATE lấy khoá ACCESS EXCLUSIVE. Đây là khoá mạnh nhất — nó chặn cả đọc lẫn ghi trên bảng cho tới khi xong. Bù lại TRUNCATE xong rất nhanh nên cửa sổ khoá ngắn, nhưng trên một bảng đang bị truy vấn liên tục, ngay cả khoảnh khắc ngắn đó cũng có thể chặn hàng loạt query. DELETE chỉ lấy khoá ROW EXCLUSIVE, các SELECT khác vẫn chạy song song.

TRUNCATE là all-or-nothing. Không có WHERE — nó luôn xoá toàn bộ bảng. Cần xoá chọn lọc (một phần dữ liệu theo điều kiện) thì bắt buộc dùng DELETE. TRUNCATE chỉ dành cho "làm sạch cả bảng".

Cẩn thận CASCADE với khoá ngoại. Nếu bảng được bảng khác tham chiếu bằng khoá ngoại, TRUNCATE sẽ báo lỗi trừ khi bạn thêm CASCADE — và CASCADE sẽ TRUNCATE luôn các bảng tham chiếu đó. Đây là con dao hai lưỡi: tiện khi cố ý dọn cả cụm bảng, nhưng nguy hiểm nếu quên rằng nó xoá lan sang bảng khác. Đọc kỹ trước khi gõ CASCADE.

Ba ý mang về

  1. TRUNCATE vứt cả tệp dữ liệu nên nhanh hơn hàng chục lần và trả chỗ ngay: đo thật, làm rỗng bảng 3 triệu dòng mất 678 ms với DELETE (413 MB WAL, 3 triệu dead tuple, bảng vẫn 331 MB) so với 19,85 ms với TRUNCATE (21 kB WAL, 0 dead tuple, bảng còn 16 kB) — nhanh ~34 lần và không để lại rác cần VACUUM.
  2. Ba khác biệt hành vi phải nhớ: TRUNCATE vẫn có giao dịch trong PostgreSQL (ROLLBACK được, khác MySQL); RESTART IDENTITY mới reset sequence còn DELETE thì không; và trigger cấp dòng ON DELETE không chạy với TRUNCATE.
  3. Chọn theo ý định: xoá sạch cả bảng thì TRUNCATE; xoá chọn lọc theo điều kiện thì bắt buộc DELETE với WHERE — và nhớ TRUNCATE lấy khoá ACCESS EXCLUSIVE (chặn cả đọc) cùng cạm bẫy CASCADE xoá lan sang bảng tham chiếu khoá ngoại.

Phần sau ta gom các thao tác giữ database khoẻ mạnh về một chỗ: Phần sau đo ba việc bảo trì định kỳ — ANALYZE cập nhật thống kê cho planner, VACUUM dọn dead tuple, và REINDEX dựng lại index phình — khi nào cần và cái giá của mỗi việc.