Phần 1 thấy một bất biến đẹp: page_size × page_count = kích thước file. Phần này đo một hệ quả bất ngờ của nó: khi bạn xóa dữ liệu, page_count không giảm, nên file không nhỏ lại. Dữ liệu biến mất nhưng đĩa vẫn bị chiếm. Hiểu vì sao — và khi nào cần VACUUM để lấy lại — là chuyện mọi người vận hành SQLite lâu dài đều gặp. Đo trong container sqlite 3.45.3.
Xóa 90% dữ liệu, file vẫn nguyên
Dựng bảng một triệu dòng, đo file, xóa 90%, đo lại:
sau khi chèn 1 triệu dòng:
file = 37 212 160 B page_count = 9085 freelist = 0
sau khi XÓA 90% (còn 100 000 dòng):
file = 37 212 160 B page_count = 9085 freelist = 8202
▲ KHÔNG đổi ▲ KHÔNG đổi ▲ 8202 trang trống
File vẫn đúng 37 MB, page_count vẫn 9085 — y nguyên. Thứ duy nhất đổi là freelist_count: nhảy từ 0 lên 8202. Khi xóa dữ liệu, SQLite không trả trang về hệ điều hành mà đánh dấu chúng là trang trống (freelist) — một danh sách các trang bên trong file sẵn sàng được dùng lại. File giữ nguyên kích thước vì các trang đó vẫn thuộc về nó, chỉ là đang rỗng.
Freelist không lãng phí: nó được tái dùng
Giữ lại các trang trống không phải sự lười biếng — đó là tối ưu. Lần tới bạn chèn dữ liệu, SQLite lấp các trang trong freelist trước, nên file không cần lớn thêm. Đo thật:
500 000 dòng → file = 7 933 952 B
xóa 50% → file = 7 933 952 B, freelist = 982 trang
chèn lại 250 000 dòng mới → file = 7 933 952 B ← KHÔNG đổi! freelist còn 27
Chèn 250.000 dòng mới mà file không lớn thêm một byte — SQLite tái dùng 982 trang trống, và freelist tụt xuống còn 27. File chỉ thực sự nở ra khi freelist cạn. Đây là lý do việc "xóa rồi chèn lại" (như xoay vòng log, dọn dữ liệu cũ) không làm file phình mãi: dung lượng đã cấp được quay vòng bên trong.

Hình 1: Xóa dữ liệu chỉ đưa trang vào freelist, không thu nhỏ file (trên); và freelist được tái dùng cho lần chèn sau nên file không phình mãi (dưới). Đo trong container sqlite 3.45.3.
VACUUM: dựng lại file để thu hồi đĩa thật
Nếu bạn xóa nhiều và không định chèn lại sớm, 8202 trang trống kia là 33 MB đĩa bị chiếm vô ích. Để lấy lại, dùng VACUUM — nó dựng lại toàn bộ CSDL thành một file mới, gọn, bỏ hết trang trống:
trước VACUUM: file = 37 212 160 B page_count = 9085 freelist = 8202
sau VACUUM: file = 3 616 768 B page_count = 883 freelist = 0
▲ 37 MB → 3,6 MB (~1/10), mất 19 ms
File co từ 37 MB xuống 3,6 MB — gần một phần mười. VACUUM chép toàn bộ dữ liệu sống sang một file tạm mới theo thứ tự gọn gàng, rồi thay file cũ; nhờ vậy nó vừa thu hồi trang trống vừa chống phân mảnh (các trang của một bảng được xếp lại liên tục, tốt cho quét tuần tự). Nhưng VACUUM không miễn phí: nó cần gấp đôi dung lượng tạm thời (file mới dựng song song với file cũ), khóa toàn bộ CSDL trong lúc chạy, và tốn thời gian tỉ lệ với kích thước dữ liệu. Nên chạy nó vào lúc rảnh, không phải trong đường nóng.
auto_vacuum: thu hồi tự động, nhưng phải chọn trước
SQLite có PRAGMA auto_vacuum để thu hồi trang trống tự động thay vì phải VACUUM tay, nhưng phải đặt trước khi tạo bảng:
auto_vacuum = NONE (0, mặc định) → trang trống ở lại freelist tới khi VACUUM tay
auto_vacuum = FULL (1) → trả trang về OS ở mỗi commit (có chi phí)
auto_vacuum = INCREMENTAL (2) → thu hồi khi gọi PRAGMA incremental_vacuum
Mặc định là NONE: trang trống nằm trong freelist cho tới khi bạn VACUUM. FULL thu hồi ngay sau mỗi giao dịch xóa — tiện nhưng thêm chi phí ghi và vẫn có thể phân mảnh. INCREMENTAL là trung dung: trang trống được theo dõi để trả lại dần khi bạn chủ động gọi incremental_vacuum, không gây giật ở mỗi commit. Đổi auto_vacuum trên CSDL đã có dữ liệu thì phải chạy một lần VACUUM để áp dụng.

Hình 2: VACUUM dựng lại file từ 37 MB xuống 3,6 MB và chống phân mảnh (trên); ba chế độ auto_vacuum để thu hồi tự động (dưới) — đo trong container sqlite 3.45.3.
Vì sao điều này quan trọng
"Xóa dữ liệu không giải phóng đĩa" là một bất ngờ khiến nhiều người tưởng CSDL hỏng khi thấy file một ứng dụng SQLite cứ phình to dù đã dọn dữ liệu. Thực ra đó là thiết kế: freelist giữ trang để tái dùng nhanh, và VACUUM là công cụ chủ động để thu hồi khi cần. Biết đọc freelist_count, biết khi nào chạy VACUUM (sau một đợt xóa lớn, lúc hệ thống rảnh), và biết auto_vacuum có thể tự động hóa việc đó là đủ để giữ một file SQLite gọn gàng lâu dài. Tới đây ta đã hiểu gần trọn vật lý và cơ chế của SQLite. Phần áp chót gom lại các núm tối ưu — cache_size, mmap_size, page_size, synchronous — và đo xem mỗi núm đổi tốc độ chèn hàng loạt bao nhiêu, để biết cái nào đáng chỉnh.