Toollance

Câu lệnh SQLite đưa query từ 5 giây xuống 0.05 giây

dev-toolssqliteperformance

Julia Evans đã đăng ghi chú về việc chạy SQLite cho một dự án thật, và trong đó có một sự thật thuộc loại giá trị nhất trên mỗi ký tự trong vận hành database: chạy ANALYZE đưa một query full-text search từ khoảng 5 giây xuống 0.05 giây.

Đó là mức thay đổi 100 lần chỉ từ một câu lệnh, không đổi schema, không thêm index mới.

Vì sao ANALYZE lại quan trọng đến thế

Query planner của SQLite quyết định dùng index nào cho một query. Để quyết định tốt, nó cần thống kê — bảng có bao nhiêu dòng, mỗi index chọn lọc đến đâu, giá trị phân bố ra sao.

Mặc định, nó không có gì cả. Nó chạy bằng heuristic và phỏng đoán.

ANALYZE điền vào bảng sqlite_stat1 các thống kê thật, và từ đó planner đưa ra lựa chọn có căn cứ. Khi cú đoán trước đó sai, khác biệt không phải là “nhanh hơn một chút” — đó là khác biệt giữa dùng index và quét toàn bảng, hoặc giữa một phép join tuyến tính và một phép join vô tình thành bậc hai. Đó là cách bạn có 100 lần trên một query bạn chưa hề đụng tới.

-- sinh thống kê cho toàn bộ database
ANALYZE;

-- hoặc cho một bảng
ANALYZE my_table;

-- xem kết quả
SELECT * FROM sqlite_stat1;

Điểm cần lưu ý là thống kê sẽ cũ đi. Chúng phản ánh dữ liệu tại thời điểm chạy gần nhất. Nạp một triệu dòng vào bảng vốn chỉ có mười nghìn dòng, và planner giờ đang tối ưu dựa trên bức ảnh của một database không còn tồn tại. Hãy chạy lại ANALYZE sau các lần nạp dữ liệu lớn và theo lịch định kỳ.

Checklist ngắn trước khi ship

Từ chính ghi chú đó, cộng thêm những gì nó hàm ý:

1. Bật WAL mode. Write-ahead logging cho phép reader tiếp tục đọc trong khi một lệnh ghi đang diễn ra. Đây gần như là khuyến nghị mặc định cho mọi database SQLite không phải loại đơn luồng chỉ đọc.

PRAGMA journal_mode = WAL;

Thiết lập này tồn tại lâu dài — nó thuộc về file database chứ không phải connection, nên bạn chỉ cần đặt một lần.

2. Chạy ANALYZE, rồi chạy lại. Như trên. Rẻ để làm, đắt nếu bỏ qua.

3. Đặt busy timeout mà bạn thực sự đã cân nhắc. SQLite chỉ cho phép đúng một writer tại một thời điểm. Khi writer thứ hai đến, nó phải chờ, và nếu chờ quá timeout thì nó fail. Evans mô tả các worker bị crash với write timeout 5 giây khi có tranh chấp. Cách xử lý là gom công việc dọn dẹp thành từng batch để không query nào giữ write lock quá giới hạn.

Con số bạn chọn là một quyết định thật. Quá ngắn thì gặp lỗi giả dưới tải bình thường; quá dài thì một writer bị kẹt sẽ làm treo cả hệ thống thay vì fail sớm.

4. Chọn cách backup trước khi cần đến nó. Hai hướng, với kiểu hỏng khác nhau:

5. Cân nhắc tách thành nhiều file database. Nếu hai nhóm bảng không bao giờ join với nhau, chúng không cần chung một file — cũng không cần chung một write lock. Đây là lối thoát thực sự bị dùng quá ít cho vấn đề tranh chấp ghi, vì giới hạn single-writer là trên mỗi file database, không phải trên mỗi process.

Giới hạn thật sự

Đường ghi là nơi SQLite thôi là câu trả lời dễ dàng. Đọc thì mở rộng rất tốt. Ghi thì tuần tự hóa, không có ngoại lệ.

Tất cả những thứ ở trên — batch, timeout, tách file — đều là mua thêm khoảng thở trước đúng một ràng buộc đó. Chúng đáng làm và đi được khá xa. Nhưng nếu bạn thấy mình đang thiết kế những cách lách ngày càng tinh vi cho tranh chấp ghi, đó là tín hiệu nên chuyển sang một database được xây quanh nhiều writer đồng thời, chứ không phải dấu hiệu bạn chưa tune đủ mạnh.

Evans lưu ý dự án nói trên quản lý khoảng 10.000 dòng mà không gặp vấn đề hiệu năng. Phần lớn dự án vội chọn Postgres ngay từ ngày đầu lẽ ra đã ổn với SQLite trong thời gian rất dài — và phần lớn dự án ở lại với SQLite mãi mãi thì chưa bao giờ chạy ANALYZE.

Việc nên làm hôm nay

Nếu bạn đang có một database SQLite chạy production ngay lúc này, hãy kết nối vào và chạy ANALYZE. Mất vài giây. Sau đó đo lại query chậm nhất của bạn.

Kết quả có khả năng cao là không có gì thay đổi, vì planner của bạn vốn đã đoán đúng. Kết quả còn lại là bạn nhận được cả một buổi chiều công việc tối ưu hiệu năng miễn phí.

Nguồn: Learning a few things about running SQLite của Julia Evans, thảo luận trên Hacker News.