EXPLAIN QUERY PLAN cho biết truy vấn sẽ chạy thế nào
Trước khi tối ưu một truy vấn chậm, bạn cần biết SQLite thực sự đang làm gì. EXPLAIN QUERY PLAN in ra bản tóm tắt ngắn về chiến lược mà bộ lập kế hoạch (query planner) đã chọn: nó đụng đến những bảng nào, theo thứ tự nào, và dùng index nào (nếu có). Bản thân truy vấn không chạy; bạn chỉ nhận được kế hoạch.
Đặt các từ khóa này trước bất kỳ câu lệnh nào:
Kết quả trông giống thế này:
QUERY PLAN
`--SEARCH users USING INDEX sqlite_autoindex_users_1 (email=?)
Một dòng đó cho bạn biết rất nhiều: SQLite đang dùng SEARCH (không phải quét), trên bảng users, dùng index unique được tạo tự động cho email, với email là khóa tra cứu. Đúng như bạn mong đợi.
SCAN và SEARCH: điều đầu tiên cần đọc
Mỗi dòng trong kế hoạch đều bắt đầu bằng SCAN hoặc SEARCH. Sự khác biệt này là tín hiệu quan trọng nhất trong toàn bộ kết quả.
SCAN <table>: SQLite đọc mọi hàng của bảng (hoặc mọi mục của một index). Chi phí tăng theo kích thước bảng.SEARCH <table> USING ...: SQLite nhảy thẳng đến các hàng khớp qua index hoặc khóa chính. Chi phí tăng theo kích thước kết quả, không phải kích thước bảng.
Đây là so sánh song song. Một cột có index, cột còn lại thì không:
Kế hoạch đầu tiên báo SEARCH orders USING INDEX idx_orders_customer. Kế hoạch thứ hai báo SCAN orders: không có index trên status, nên SQLite đọc mọi hàng. Trên bảng nhỏ thì không thấy khác biệt; trên bảng một triệu hàng, đó là chênh lệch giữa vài mili giây và vài giây.
SCAN không phải lúc nào cũng sai. Với các bảng tra cứu rất nhỏ, hay những truy vấn thực sự trả về phần lớn các hàng, quét bảng là kế hoạch đúng. Nhưng trên bảng lớn với điều kiện lọc có tính chọn lọc cao, SCAN là dấu hiệu bạn nên thêm index.
Xác nhận index đang được dùng
Cụm cần tìm là USING INDEX <name> (hoặc USING COVERING INDEX <name>, sẽ nói thêm ở dưới). Nếu bạn tạo index với hy vọng bộ lập kế hoạch sẽ dùng nó, đây là cách kiểm tra:
Bạn sẽ thấy SEARCH events USING INDEX idx_events_user (user_id=?). Nếu thay vào đó kế hoạch báo SCAN events, có gì đó đang ngăn bộ lập kế hoạch dùng index. Nguyên nhân thường gặp là bọc cột trong một hàm (WHERE lower(user_id) = ...), so sánh giữa các kiểu khác nhau, hoặc dùng LIKE '%foo%' với ký tự đại diện ở đầu.
Thử nhanh điều đó:
Phần + 0 đó vô hiệu hóa index: kế hoạch quay về SCAN events. Mọi biểu thức trên cột có index đều gây ra kết quả tương tự.
Covering index hiển thị khác
Khi một index chứa mọi cột mà truy vấn cần, SQLite có thể trả lời truy vấn chỉ từ index mà không cần đụng đến bảng. Kế hoạch báo USING COVERING INDEX:
Kế hoạch: SEARCH products USING COVERING INDEX idx_products_sku_price (sku=?). Truy vấn cần price, index đã lưu sẵn sku và price, nên SQLite không bao giờ đọc bảng gốc. Covering index là kế hoạch nhanh nhất bạn có thể có cho một lần tra cứu, đáng để biết khi chọn những cột nào sẽ đánh index cùng nhau.
Đọc kế hoạch của phép join
Phép join là nơi kế hoạch trở nên thú vị. Mỗi dòng trong kế hoạch tương ứng với một bảng trong phép join, và thứ tự các dòng là thứ tự SQLite duyệt các bảng. Bảng đầu tiên là bảng ngoài; các bảng sau được tra cứu một lần cho mỗi hàng của bảng ngoài.
Một kế hoạch điển hình:
QUERY PLAN
|--SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
`--SEARCH o USING INDEX idx_orders_customer (customer_id=?)
Đọc từ trên xuống: SQLite tìm một khách hàng duy nhất bằng khóa chính, rồi với khách hàng đó, tra cứu các đơn hàng khớp qua index trên customer_id. Cả hai dòng đều là SEARCH, không có lần quét toàn bộ nào, đúng như bạn muốn.
Nếu bạn thấy SCAN o ở dòng thứ hai, mỗi lần tra cứu khách hàng sẽ kéo theo một lượt quét toàn bộ orders. Trên bảng lớn, đó là thảm họa. Cách sửa gần như luôn là thêm index trên cột dùng để join.
Truy vấn kết hợp và truy vấn con
Kế hoạch cho UNION, EXCEPT và truy vấn con (subquery) được lồng nhau. Mỗi nhánh hiển thị thụt lề bên dưới nút cha:
Bạn sẽ thấy hai hàng con dưới tiêu đề COMPOUND QUERY, mỗi hàng cho một nhánh. Truy vấn con và CTE cũng tương tự: mỗi cái có một nút kế hoạch thụt lề riêng, và bạn đọc từng nút với cùng góc nhìn SCAN hay SEARCH.
Truy vấn con trở thành một nút kế hoạch riêng ("LIST SUBQUERY" hoặc tương tự), với chiến lược truy cập riêng. Áp dụng cùng các bước kiểm tra ở mọi cấp.
EXPLAIN và EXPLAIN QUERY PLAN
Đây là hai thứ khác nhau, và nhiều người hay nhầm lẫn.
EXPLAIN (không có QUERY PLAN) in ra bytecode mà máy ảo của SQLite sẽ thực thi: hàng chục opcode cấp thấp như OpenRead, SeekRowid, Column, ResultRow. Hữu ích nếu bạn đang gỡ lỗi chính engine. Gần như không bao giờ hữu ích khi tối ưu.
EXPLAIN QUERY PLAN là bản tóm tắt dễ đọc mà bạn thực sự cần. Khi phân vân, hãy luôn chọn EXPLAIN QUERY PLAN.
Quy trình xử lý truy vấn chậm
Khi một truy vấn chạy chậm, vòng lặp xử lý trông như sau:
- Chạy
EXPLAIN QUERY PLANtrên truy vấn đó. - Với mỗi dòng của bảng, tự hỏi: đây là
SCANhaySEARCH? Trên bảng lớn,SCANlà nghi phạm. - Nếu một
SCANđang lọc theo cột nào đó, cân nhắc tạo index trên cột đó. - Với phép join, xác nhận các bảng ở vòng lặp trong dùng
SEARCH USING INDEXtrên cột join. - Chạy lại
EXPLAIN QUERY PLANsau khi thêm index. Kế hoạch phải thay đổi. Nếu không, bộ lập kế hoạch đã quyết định index của bạn không đáng dùng, thường vì bảng nhỏ hoặc điều kiện lọc không đủ chọn lọc.
Ví dụ cụ thể cho bước 5:
Kế hoạch đã đổi từ SCAN sang SEARCH. Đó là tín hiệu index đang làm đúng việc của nó. (Trên một bảng mới, gần như trống, bộ lập kế hoạch có thể vẫn quét vì chưa đủ dữ liệu để đáng dùng index: hãy thêm dữ liệu vào bảng hoặc chạy ANALYZE, lựa chọn thường sẽ đổi.)
Những gì kế hoạch không cho bạn biết
EXPLAIN QUERY PLAN mô tả chiến lược, không phải chi phí. Nó không cho bạn biết truy vấn mất 800 ms hay trả về 50.000 hàng. Để biết điều đó, bạn cần đo thời gian (.timer on trong CLI) và đếm số hàng. Kế hoạch và thời gian bổ sung cho nhau: kế hoạch cho biết vì sao truy vấn chậm, bộ đếm giờ cho biết truy vấn có chậm hay không.
Thêm hai giới hạn nên biết:
- Kế hoạch có thể thay đổi khi dữ liệu tăng lên. Một truy vấn quét bảng 100 hàng vẫn ổn sẽ cần index khi bảng lên đến một triệu hàng. Hãy kiểm tra lại kế hoạch trên dữ liệu cỡ production, không phải dữ liệu mẫu khi phát triển.
- Bộ lập kế hoạch dùng thống kê do
ANALYZEthu thập. Không có chúng, nó dùng các giá trị mặc định không phải lúc nào cũng tốt. Thống kê cũ hoặc thiếu là lý do phổ biến của những kế hoạch bất ngờ.
Tiếp theo: ANALYZE và VACUUM
Bộ lập kế hoạch đưa ra quyết định dựa trên thống kê về bảng và index của bạn. Nếu thống kê bị thiếu hoặc lỗi thời, ngay cả một cấu trúc có index hoàn hảo cũng có thể cho ra kế hoạch tệ. ANALYZE là cách giữ chúng luôn mới, còn VACUUM là lệnh đi kèm để thu hồi dung lượng và chống phân mảnh tệ cơ sở dữ liệu. Đó là nội dung tiếp theo.
Câu hỏi thường gặp
EXPLAIN QUERY PLAN trong SQLite dùng để làm gì?
Nó yêu cầu SQLite mô tả cách sẽ thực thi một truy vấn mà không thực sự chạy truy vấn đó. Kết quả cho biết bảng nào bị quét, index nào được dùng và các bảng trong phép join được duyệt theo thứ tự nào. Đặt EXPLAIN QUERY PLAN trước bất kỳ câu SELECT, INSERT, UPDATE hay DELETE nào để xem kế hoạch.
SCAN và SEARCH trong kết quả khác nhau thế nào?
SCAN nghĩa là SQLite đọc mọi hàng của bảng hoặc index: ổn với bảng nhỏ, tốn kém với bảng lớn. SEARCH nghĩa là SQLite nhảy thẳng đến các hàng khớp nhờ index hoặc khóa chính. Trên bảng lớn, bạn gần như luôn muốn thấy SEARCH cho các cột dùng để lọc.
Làm sao kiểm tra truy vấn của mình có dùng index không?
Chạy EXPLAIN QUERY PLAN trên truy vấn và tìm USING INDEX <name> hoặc USING COVERING INDEX <name> trong kết quả. Nếu chỉ thấy SCAN <table> mà không nhắc đến index nào, truy vấn đang quét toàn bộ bảng và một index nhiều khả năng sẽ giúp ích.
EXPLAIN và EXPLAIN QUERY PLAN khác nhau thế nào?
EXPLAIN hiển thị bytecode cấp thấp mà máy ảo của SQLite tạo ra: hữu ích khi tìm hiểu bên trong engine, hiếm khi hữu ích khi tối ưu truy vấn. EXPLAIN QUERY PLAN hiển thị bản tóm tắt dễ đọc về cách truy cập bảng và dùng index. Khi tối ưu hiệu năng, gần như lúc nào bạn cũng cần EXPLAIN QUERY PLAN.