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.

Ảnh chụp sơ đồ nền tối minh họa tách đóng góp từng núm tối ưu chèn hàng loạt trong container sqlite 3.45.3. Phần trên con số lớn: chèn 100 nghìn dòng mặc định mỗi dòng tự commit nhật ký DELETE mất 153228 mili-giây, cùng dữ liệu tối ưu WAL synchronous NORMAL một giao dịch cache lớn chỉ 79 mili-giây, nhanh gần hai nghìn lần. Phần dưới tách từng núm trên 10 nghìn dòng: mặc định 7768 mili-giây, chỉ cần gói trong MỘT giao dịch còn 10 mili-giây gần 800 lần là gần hết phép màu, thêm WAL 14, thêm synchronous NORMAL 11; nhấn mạnh chi phí ghi do số lần fsync, gói giao dịch biến mười nghìn cặp fsync thành một

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.

Ảnh chụp sơ đồ nền tối hai phần đo trong container sqlite 3.45.3. Phần trên cache_size và mmap: quét tuần tự bảng 53 MB với mmap_size 0 dùng read 53 mili-giây so với mmap_size 1 GB memory-mapped 51 mili-giây gần như không đổi; cache_size mặc định âm 2000 nghĩa 2 MB, số âm là KB số dương là số trang; chú thích hai núm này chỉ đáng kể khi dữ liệu nóng lớn hơn cache trên đĩa chậm, trên lưu trữ nhanh lợi ích mờ nhạt, núm nào đáng chỉnh tùy nút cổ chai thật. Phần dưới công thức nạp dữ liệu nhanh: journal_mode WAL, synchronous NORMAL, temp_store MEMORY, cache_size âm 20000, gói BEGIN COMMIT; thêm page_size cho quét hay ghi, PRAGMA optimize giữ thống kê tươi, tất cả chỉ là câu PRAGMA lúc mở kết nối không đổi mã

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.

Phần sau