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)

Ảnh chụp đoạn mã SQL nền tối minh hoạ JIT biên dịch biểu thức truy vấn thành mã máy ngay lúc chạy, bình thường PostgreSQL thông dịch biểu thức từng dòng có chi phí thông dịch mỗi lần JIT dùng LLVM biên dịch chúng thành mã máy một lần rồi chạy nhanh hơn nhưng biên dịch tốn thời gian trả trước, 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 jit_above_cost 100000 cost lớn hơn thì biên dịch biểu thức jit_optimize_above_cost 500000 tối ưu jit_inline_above_cost 500000 inline hàm, xem chi phí JIT trong EXPLAIN ANALYZE khối JIT Timing Functions 3 Total 31 ms thời gian biên dịch trả trước, cạm bẫy JIT kích hoạt theo cost ước tính nếu planner ước lượng sai cost cao nhưng thực ra ít dòng JIT vẫn biên dịch cộng hàng chục ms vô ích vào truy vấn lẽ ra chạy micro giây đây là lý do JIT hay làm chậm, SET jit off hoặc nâng jit_above_cost cho hệ OLTP nhiều truy vấn ngắn

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:

Ảnh chụp kết quả đo thật nền tối JIT trên pgbench_accounts 5 triệu dòng PostgreSQL 16 LLVM, ca 1 cạm bẫy query nhanh nhưng JIT bị kích hoạt cost ước tính cao SELECT count từ pgbench_accounts WHERE aid nhỏ hơn 100 JIT OFF 0,045 mili giây JIT ON 37,7 mili giây JIT Timing Inlining 26,7 cộng Emission 2,9 Total 31 ms JIT làm query chậm khoảng 800 lần toàn bộ là chi phí biên dịch vô ích, ca 2 aggregate phức tạp nhiều biểu thức trên cả 5 triệu dòng JIT OFF khoảng 907 mili giây thông dịch biểu thức JIT ON khoảng 1000 mili giây chậm hơn khoảng 10 phần trăm compile 3 ms cộng mã không nhanh hơn ngay cả trên aggregate 5 triệu dòng JIT vẫn không thắng ở đây biểu thức không đủ phức tạp để mã biên dịch bù lại chi phí và libm sqrt cos không inline được, khi nào JIT giúp truy vấn phân tích OLAP rất nhiều biểu thức số học đơn giản nhân hàng chục triệu dòng chạy lâu giây chi phí compile nhỏ so với thời gian tiết kiệm hại truy vấn OLTP ngắn hoặc cost ước tính sai kích hoạt JIT nhầm hay gặp nhấ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ề

  1. 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: Timing cho thấy 31 ms biên dịch trên một query lẽ ra 0,045 ms.
  2. 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 < 100 bị 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.
  3. 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_cost cao hoặc tắt, và luôn kiểm khối JIT: Timing trong 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.