Từ PostgreSQL 11, có một tính năng tăng tốc bật sẵn mà nhiều người không biết mình đang dùng: JIT (Just-In-Time compilation). Nó biên dịch các biểu thức trong truy vấn thành mã máy ngay lúc chạy, thay vì "thông dịch" từng dòng. Ý tưởng hay — nhưng JIT là một trong những tính năng gây tranh cãi nhất, vì nó thường xuyên làm chậm truy vấn. Bài này đo cả hai mặt, gồm một cái bẫy biến truy vấn 0,045 ms thành 37,7 ms.
JIT làm gì, và kích hoạt thế nào
Bình thường, khi đánh giá abalance * 1.1 + aid % 7 cho mỗi dòng, PostgreSQL đi qua một bộ thông dịch biểu thức — có chi phí cố định mỗi lần gọi. JIT (dùng LLVM) biên dịch biểu thức đó thành mã máy một lần, rồi chạy mã đó cho mọi dòng, nhanh hơn. Nhưng biên dịch bằng LLVM tốn thời gian trả trước — hàng chục mili-giây.
Điểm mấu chốt gây rắc rối: JIT kích hoạt theo chi phí ước tính của truy vấn, không theo lợi ích thật.
jit = on -- bật mặc định từ PG11
jit_above_cost = 100000 -- cost > mức này → biên dịch biểu thức
jit_optimize_above_cost = 500000 -- cost > mức này → tối ưu (đắt hơn)
jit_inline_above_cost = 500000 -- cost > mức này → inline hàm (đắt nhất)

Hình 1: JIT biên dịch biểu thức thành mã máy, kích hoạt khi cost ước tính vượt ngưỡng. Khối JIT: Timing trong EXPLAIN ANALYZE cho thấy thời gian biên dịch trả trước.
Cái bẫy: JIT làm query 0,045 ms thành 37,7 ms
Đây là mặt tối nổi tiếng nhất của JIT. Một truy vấn nhanh, nhưng vì lý do nào đó cost ước tính vượt ngưỡng, JIT bị kích hoạt:

Hình 2: JIT làm chậm ở cả hai ca. Query nhanh aid < 100: 0,045 ms (off) thành 37,7 ms (JIT bật) — chậm ~800 lần, toàn bộ là chi phí biên dịch. Aggregate 5 triệu dòng: 907 ms (off) so với ~1.000 ms (on), chậm 10%.
Đo thật: SELECT count(*) FROM pgbench_accounts WHERE aid < 100 chạy trong 0,045 ms khi JIT tắt. Khi JIT bị kích hoạt, cùng query mất 37,7 ms — khối JIT: Timing cho thấy Total 31 ms chỉ để biên dịch. Chậm ~800 lần, và toàn bộ là chi phí biên dịch vô ích cho một query xử lý vài dòng.
Điều này xảy ra trong thực tế khi nào? Khi planner ước lượng cost sai (thống kê cũ, cột tương quan — đúng các nguyên nhân ở bài trước): nó nghĩ query đắt, cost vượt jit_above_cost, JIT kích hoạt, rồi query thực ra chỉ chạm vài dòng. Chi phí biên dịch 30 ms bị cộng vào một query lẽ ra micro-giây. Ước lượng sai không chỉ chọn nhầm kế hoạch — nó còn kích hoạt JIT nhầm.
Ngay cả trên aggregate lớn, JIT không luôn thắng
Người ta hay nghĩ JIT thắng khi có nhiều dòng. Đo thật một aggregate phức tạp nhiều biểu thức trên cả 5 triệu dòng:
- JIT OFF: ~907 ms.
- JIT ON: ~1.000 ms — chậm hơn ~10%.
Ngay cả ở quy mô này, JIT vẫn không thắng: biểu thức không đủ phức tạp để mã biên dịch bù lại chi phí, và các hàm thư viện C (sqrt, cos) không inline được. JIT chỉ thật sự có lợi trong một cửa sổ hẹp.
Khi nào JIT thật sự giúp
Truy vấn phân tích (OLAP) rất nhiều biểu thức số học đơn giản, trên hàng chục triệu dòng, chạy lâu (nhiều giây). Khi thời gian thực thi tính bằng giây, chi phí biên dịch 30-60 ms là không đáng kể, và mã máy tăng tốc phần đánh giá biểu thức lặp đi lặp lại đủ để bù lại. Đây là bối cảnh JIT được thiết kế cho: data warehouse, báo cáo nặng.
Nó hại ở OLTP và khi ước lượng sai. Hệ thống nhiều truy vấn ngắn (OLTP) không nên bật JIT rộng rãi — mỗi lần kích hoạt nhầm là 30 ms lãng phí. Và cost ước tính sai là thủ phạm phổ biến nhất.
Đánh đổi và cách xử lý
Luôn kiểm khối JIT: Timing trong EXPLAIN ANALYZE. Nếu Total của JIT chiếm phần lớn Execution Time, JIT đang hại. Đó là bằng chứng để tắt hoặc nâng ngưỡng.
Nâng jit_above_cost thay vì tắt hẳn. Trên nhiều hệ thống, để jit = on nhưng nâng jit_above_cost lên rất cao (ví dụ 500000 hoặc hơn) là cân bằng tốt: JIT chỉ kích hoạt cho truy vấn thực sự khổng lồ, tránh cái bẫy kích hoạt nhầm. Nhiều DBA production làm vậy.
Cân nhắc tắt JIT cho OLTP. Nếu tải chủ yếu là truy vấn ngắn, SET jit = off ở cấp cơ sở dữ liệu hoặc phiên thường là lựa chọn an toàn — bạn hiếm khi mất gì và tránh được rủi ro kích hoạt nhầm.
Giữ thống kê chính xác giảm rủi ro JIT. Vì JIT kích hoạt theo cost ước tính, thống kê đúng (ANALYZE, extended statistics) không chỉ cho kế hoạch tốt mà còn tránh JIT kích hoạt nhầm do cost bị thổi phồng.
Ba ý mang về
- JIT biên dịch biểu thức thành mã máy nhưng tốn thời gian trả trước, và kích hoạt theo cost ước tính chứ không theo lợi ích thật — đo thật, khối
JIT: Timingcho thấy 31 ms biên dịch trên một query lẽ ra 0,045 ms. - Cái bẫy phổ biến nhất là ước lượng cost sai kích hoạt JIT nhầm: query nhanh
aid < 100bị JIT làm chậm từ 0,045 ms thành 37,7 ms (~800 lần) — và ngay cả aggregate 5 triệu dòng JIT vẫn chậm hơn 10% ở đây. - JIT chỉ giúp cửa sổ hẹp (OLAP nhiều biểu thức × hàng chục triệu dòng, chạy nhiều giây); với OLTP nên nâng
jit_above_costcao hoặc tắt, và luôn kiểm khốiJIT: Timingtrong EXPLAIN để biết nó đang giúp hay hại.
Phần sau ta quay về một thói quen viết SQL tưởng vô hại nhưng tốn kém: Phần sau đo vì sao SELECT * là một phản mẫu — nó lấy thừa cột, phá index-only scan, và làm chậm truyền dữ liệu.