Suốt mười bài, mỗi lần đo tốc độ ta đều gặp lại vài cái tên: journal_mode, synchronous, cache_size, và chuyện gói giao dịch. Bài áp chót gom chúng lại một chỗ và trả lời câu thực dụng: nếu chỉ chỉnh một thứ để SQLite nhanh hơn, chỉnh cái nào? Câu trả lời, đo trong container sqlite 3.45.3, có thể gây bất ngờ — và nó không phải cache hay mmap như nhiều người tưởng.
Trước và sau: 153 giây xuống 79 mili-giây
Bắt đầu bằng con số lớn nhất. Chèn 100.000 dòng theo hai cách — mặc định hoàn toàn, và bật mọi núm tối ưu hợp lý:
100 000 dòng, mặc định (mỗi dòng tự commit, nhật ký DELETE): 153 228 ms
100 000 dòng, tối ưu (WAL + synchronous=NORMAL + 1 giao dịch + cache lớn): 79 ms
153 giây xuống 79 mili-giây — nhanh gần hai nghìn lần. Cùng một cái máy, cùng một bảng, cùng số dòng. Toàn bộ khác biệt đến từ cấu hình. Nhưng "bật mọi núm" không cho biết núm nào làm nên phép màu. Hãy tách ra.
Tách từng núm: một núm làm gần hết việc
Thêm từng núm một, đo 10.000 dòng mỗi lần:
mặc định (DELETE, mỗi dòng tự commit): 7768 ms
+ gói trong MỘT giao dịch: 10 ms ← gần như toàn bộ phép màu ở đây
+ thêm WAL: 14 ms
+ thêm synchronous=NORMAL: 11 ms
Nhìn dòng thứ hai: chỉ cần gói các lệnh chèn vào một giao dịch BEGIN ... COMMIT, thời gian rơi từ 7768 ms xuống 10 ms — gần 800 lần. WAL và synchronous=NORMAL thêm vào chỉ là dao động nhỏ ở quy mô này. Lý do ta đã đo ở phần 6: chi phí ghi bị chi phối bởi số lần fsync, và gói giao dịch biến mười nghìn cặp fsync thành một. Đây là núm tối ưu đáng giá nhất của SQLite, và nó miễn phí về độ an toàn.

Hình 1: Tối ưu chèn hàng loạt — 153 giây xuống 79 ms khi bật mọi núm (trên); và tách ra cho thấy chỉ riêng gói vào một giao dịch đã làm gần hết việc, 7768 ms xuống 10 ms (dưới). Đo trong container sqlite 3.45.3.
cache_size và mmap: núm nghe kêu, hiệu quả tùy đĩa
Hai núm hay được nhắc tới khi tối ưu SQLite là cache_size (bộ đệm trang trong RAM) và mmap_size (ánh xạ file vào bộ nhớ thay vì gọi read()). Nhưng đo trên quét tuần tự một bảng 53 MB:
mmap_size = 0 (dùng read()): 53 ms / quét toàn bảng
mmap_size = 1 GB (memory-mapped): 51 ms / quét toàn bảng
Khác biệt gần như bằng không. cache_size mặc định là -2000 (số âm nghĩa là 2000 KB = 2 MB; số dương nghĩa là số trang), và tăng nó lên trên lưu trữ nhanh của container này cũng không đổi mấy. Vì sao? Vì trong môi trường đo này, "đĩa" thực chất rất nhanh (gần như RAM), nên việc giảm số lần đọc đĩa hay đổi cách đọc không cứu được nhiều thời gian. cache_size và mmap_size chỉ thực sự đáng kể khi tập dữ liệu nóng lớn hơn cache và nằm trên đĩa chậm — khi đó một cache lớn giữ được nhiều trang nóng trong RAM, cắt các lần đọc đĩa lặp lại. Trên SSD nhanh hay container, lợi ích của chúng mờ nhạt.
Đây là bài học quan trọng về đo hiệu năng: núm nào đáng chỉnh phụ thuộc vào nút cổ chai thật của hệ thống bạn. Trên tải ghi, nút cổ chai là fsync — nên giao dịch và synchronous thắng lớn. Trên tải đọc với đĩa chậm và dữ liệu lớn, nút cổ chai là I/O đọc — khi đó cache_size mới lên tiếng. Đừng chỉnh mù; hãy đo trên đúng lưu trữ của mình.
Công thức nạp dữ liệu nhanh và các núm còn lại
Gộp lại, đây là cấu hình nên dùng khi nạp một lượng lớn dữ liệu vào SQLite:
PRAGMA journal_mode = WAL; -- ghi tuần tự, đọc-ghi song song
PRAGMA synchronous = NORMAL; -- an toàn với WAL, ít fsync hơn FULL
PRAGMA temp_store = MEMORY; -- bảng tạm và chỉ mục sắp xếp nằm trong RAM
PRAGMA cache_size = -20000; -- 20 MB cache (giúp nếu đĩa chậm)
BEGIN;
-- hàng nghìn/triệu lệnh INSERT ở đây
COMMIT;
Vài núm còn lại đáng biết: page_size (phần 1) ảnh hưởng số dòng mỗi trang và độ phẳng cây — trang lớn tốt cho quét tuần tự, nhỏ tốt cho ghi lẻ; đổi được trước khi tạo bảng hoặc qua một lần VACUUM. temp_store = MEMORY đẩy bảng tạm và sắp xếp vào RAM, hữu ích cho truy vấn nặng ORDER BY/GROUP BY. Và PRAGMA optimize (phần 5) chạy khi đóng kết nối để giữ thống kê tươi. Không núm nào trong số này cần đổi mã ứng dụng — chỉ vài câu PRAGMA lúc mở kết nối.

Hình 2: mmap và cache_size ít tác dụng trên lưu trữ nhanh (trên); và công thức PRAGMA để nạp dữ liệu nhanh cùng các núm còn lại (dưới) — đo trong container sqlite 3.45.3.
Vì sao điều này quan trọng
Tối ưu SQLite thường bị hiểu sai thành "tăng cache cho to". Phép đo nói khác: trên tải ghi — đúng kiểu mà người ta hay than SQLite chậm — núm thắng áp đảo là gói giao dịch cộng synchronous/journal_mode, vì chúng cắt số lần fsync; còn cache_size và mmap_size chỉ lên tiếng khi I/O đọc là nút cổ chai thật. Bài học lớn hơn chính là tinh thần của cả sê-ri: đừng tin lời khuyên tối ưu chung chung, hãy đo trên hệ thống của mình. Tới đây ta đã mổ xẻ SQLite từ định dạng tệp tới các núm tối ưu, tất cả bằng con số thật. Phần cuối gom lại toàn bộ hành trình, và trả lời câu hỏi thực dụng nhất: với tất cả những gì đã đo, khi nào nên chọn SQLite, và khi nào không.