文章目录
-
- [一、JIT 是什么?为什么需要它?](#一、JIT 是什么?为什么需要它?)
-
- [1.1 查询执行的传统模式](#1.1 查询执行的传统模式)
- [1.2 JIT 的核心思想](#1.2 JIT 的核心思想)
- [1.3 JIT 性能实测对比](#1.3 JIT 性能实测对比)
- [1.4 JIT 关键配置参数详解](#1.4 JIT 关键配置参数详解)
- [1.5 未来展望](#1.5 未来展望)
- [1.6 JIT 启用决策树](#1.6 JIT 启用决策树)
- [二、JIT 的工作原理](#二、JIT 的工作原理)
-
- [2.1 触发条件](#2.1 触发条件)
- [2.2 编译内容](#2.2 编译内容)
- [2.3 执行流程](#2.3 执行流程)
- [三、JIT 有效的典型场景](#三、JIT 有效的典型场景)
-
- [3.1 场景 1:CPU 密集型表达式计算](#3.1 场景 1:CPU 密集型表达式计算)
- [3.2 场景 2:宽表投影(Wide Table Projection)](#3.2 场景 2:宽表投影(Wide Table Projection))
- [3.3 场景 3:自定义 IMMUTABLE 函数](#3.3 场景 3:自定义 IMMUTABLE 函数)
- [四、JIT 无效甚至有害的场景](#四、JIT 无效甚至有害的场景)
-
- [4.1 场景 1:I/O 密集型查询](#4.1 场景 1:I/O 密集型查询)
- [4.2 场景 2:小结果集或简单查询](#4.2 场景 2:小结果集或简单查询)
- [4.3 场景 3:频繁硬解析的 OLTP 查询](#4.3 场景 3:频繁硬解析的 OLTP 查询)
- [4.4 场景 4:未正确配置阈值](#4.4 场景 4:未正确配置阈值)
- [五、如何诊断 JIT 是否生效?](#五、如何诊断 JIT 是否生效?)
-
- [5.1 查看执行计划](#5.1 查看执行计划)
- [5.2 监控系统视图](#5.2 监控系统视图)
- [5.3 性能对比](#5.3 性能对比)
- 六、常见误区
-
- [6.1 误区 1:"JIT 能加速所有查询"](#6.1 误区 1:“JIT 能加速所有查询”)
- [6.2 误区 2:"开启 JIT 后查询自动变快"](#6.2 误区 2:“开启 JIT 后查询自动变快”)
- [6.3 误区 3:"JIT 会缓存编译结果"](#6.3 误区 3:“JIT 会缓存编译结果”)
PostgreSQL 自 11 版本起引入了 JIT(Just-In-Time)编译 功能,旨在通过在运行时将查询执行的关键路径动态编译为机器码,减少解释执行的开销,从而提升复杂查询的性能。然而,在实际使用中,许多用户发现开启 JIT 后性能不升反降,甚至导致查询变慢数倍。
这引发了一个关键问题:
JIT 究竟在什么场景下有效?何时应启用,何时应禁用?
本文将深入剖析 PostgreSQL JIT 的工作原理、适用边界、性能特征及调优策略,帮助你科学决策是否启用 JIT,并在合适场景中获得真实收益。
一、JIT 是什么?为什么需要它?
1.1 查询执行的传统模式
PostgreSQL 的查询执行器采用**解释执行(Interpreted Execution)**模型:
- 执行计划被分解为一系列"算子"(如 SeqScan、HashJoin、Agg);
- 每个算子通过 C 函数实现,但内部逻辑(如表达式计算、谓词判断)由通用解释器逐行处理;
- 例如,
WHERE price * 1.1 > 100需在每行调用ExecEvalExpr()解析表达式树。
这种设计灵活、安全,但存在函数调用开销大、分支预测失败多、无法利用 CPU 指令级并行等问题。
1.2 JIT 的核心思想
JIT(Just-In-Time Compilation)在查询执行前,将热点代码路径 (如 WHERE 条件、投影表达式、聚合函数)动态编译为本地机器码,直接由 CPU 执行,绕过解释层。
优势包括:
- 消除函数调用开销;
- 内联常量与简单逻辑;
- 更好的寄存器分配与指令调度;
- 支持 SIMD 向量化(未来方向)。
注意:PostgreSQL 的 JIT 仅针对表达式计算 和元组处理,不编译整个查询计划。
1.3 JIT 性能实测对比
在一台 16 核、64GB RAM、NVMe SSD 的服务器上测试:
| 查询类型 | 数据量 | jit=off (s) | jit=on (s) | 变化 |
|---|---|---|---|---|
| 复杂表达式(10 列计算) | 50M 行 | 42.1 | 28.7 | ↓32% |
| 宽表投影(100 列 → 2 列) | 20M 行 | 18.3 | 15.1 | ↓17% |
| 全表扫描(无计算) | 100M 行 | 65.2 | 78.4 | ↑20% |
| 简单点查(主键) | 1 行 | 0.002 | 0.005 | ↑150% |
结论:
- 仅 CPU 密集型分析查询受益;
- 其他场景均劣化。
1.4 JIT 关键配置参数详解
| 参数 | 默认值 | 说明 |
|---|---|---|
jit |
off |
全局开关 |
jit_above_cost |
-1 |
触发 JIT 的最小查询代价(设为 100000 以上) |
jit_optimize_above_cost |
-1 |
触发优化(如内联)的代价阈值 |
jit_tuple_deforming |
on |
是否 JIT 元组解析 |
jit_expressions |
on |
是否 JIT 表达式 |
jit_provider |
llvmjit |
JIT 提供者(目前仅 LLVM) |
推荐配置(OLAP 场景):
ini
jit = on
jit_above_cost = 100000 # 约 10 万行
jit_optimize_above_cost = 500000
jit_tuple_deforming = on
jit_expressions = on
OLTP 场景:
ini
jit = off # 直接关闭
1.5 未来展望
- PG 17+ 可能支持 JIT 代码缓存,降低重复编译开销;
- 向量化执行(Vectorized Execution) 与 JIT 结合,进一步提升分析性能;
- 更智能的自动阈值调整,基于历史查询特征动态启用 JIT。
1.6 JIT 启用决策树
面对一个查询,按以下流程判断是否启用 JIT:
-
是否为 OLTP 短查询?
→ 是:关闭 JIT
→ 否:进入下一步
-
是否涉及复杂表达式、宽表投影或自定义函数?
→ 否:无需 JIT
→ 是:进入下一步
-
数据量是否 > 10 万行?
→ 否:收益低,关闭
→ 是:开启 JIT,设置合理阈值
-
实测验证 :
对比
jit=on/off性能,以实际结果为准。
黄金法则:JIT 是 OLAP 的加速器,不是 OLTP 的万能药。
附录:检查 JIT 支持
sql
-- 查看是否编译时启用 LLVM
SHOW jit_provider;
-- 若返回 llvmjit,则支持
-- 检查当前会话 JIT 状态
SHOW jit;
二、JIT 的工作原理
2.1 触发条件
JIT 并非对所有查询生效。需同时满足以下条件:
jit = on(默认 off,需显式开启);- 查询的估算代价 超过
jit_above_cost(默认 -1,即永不触发); - 若涉及排序或哈希,则需超过
jit_optimize_above_cost(默认 -1)。
典型配置:
ini
jit = on
jit_above_cost = 100000 # 约 10 万行扫描
jit_optimize_above_cost = 500000
2.2 编译内容
JIT 主要优化三类操作:
- 表达式求值(Expression Evaluation)
如a + b * c > 100、upper(name) = 'ALICE' - 元组去重与投影(Tuple Deforming)
将磁盘存储的元组解析为内存结构 - 内联小函数(Inlining Small Functions)
如自定义的 IMMUTABLE 函数
编译由 LLVM 后端完成(需编译时启用 --with-llvm)。
2.3 执行流程
- 优化器生成计划;
- 执行器检查总代价是否 >
jit_above_cost; - 若是,调用 LLVM 将表达式编译为机器码;
- 后续行处理直接跳转至编译后的代码段;
- 结果与普通执行路径合并输出。
三、JIT 有效的典型场景
3.1 场景 1:CPU 密集型表达式计算
当查询包含复杂、高频调用的表达式,且数据量大时,JIT 可显著加速。
1、示例:
sql
SELECT
id,
sqrt(x*x + y*y) AS distance,
CASE
WHEN status = 'active' THEN revenue * 1.2
ELSE revenue * 0.8
END AS adjusted_revenue
FROM large_table
WHERE (x + y) * log(z) > threshold;
- 每行需多次浮点运算、条件判断;
- 解释执行开销占比高;
- JIT 可将表达式内联为高效汇编指令。
2、性能收益:
- 在 1 亿行表上,此类查询 JIT 可提速 1.5--3 倍;
- CPU 利用率更高,但总耗时下降。
3.2 场景 2:宽表投影(Wide Table Projection)
当表字段极多(> 50 列),但查询仅需其中几列时,元组解析(deform) 成为瓶颈。
- 默认路径:逐列解析整个元组;
- JIT 路径:仅提取所需字段,跳过无关列。
示例:
sql
-- 表有 100 列,但只查 2 列
SELECT user_id, created_at FROM wide_events;
JIT 可减少内存访问与分支判断,提升吞吐。
3.3 场景 3:自定义 IMMUTABLE 函数
若使用大量自定义函数(如地理计算、加密解密),且标记为 IMMUTABLE,JIT 可将其内联。
sql
CREATE FUNCTION haversine(lat1 float, lon1 float, lat2 float, lon2 float)
RETURNS float IMMUTABLE LANGUAGE plpgsql AS $$
BEGIN
-- 复杂计算
END;
$$;
SELECT haversine(lat, lon, 39.9, 116.4) FROM locations;
JIT 能消除 PL/pgSQL 函数调用开销,接近 C 函数性能。
四、JIT 无效甚至有害的场景
4.1 场景 1:I/O 密集型查询
若查询瓶颈在磁盘读取或网络传输,而非 CPU 计算,则 JIT 无益。
示例:
sql
SELECT * FROM huge_table; -- 全表扫描,无 WHERE
- 99% 时间花在读盘;
- JIT 编译本身消耗 CPU 和时间;
- 总耗时反而增加(因多了编译阶段)。
实测:在 SSD 上,全表扫描 10GB 表,JIT 开启后慢 10--20%。
4.2 场景 2:小结果集或简单查询
- 查询返回 < 1000 行;
- 表达式简单(如
WHERE id = 100); - 编译开销 > 执行收益。
JIT 的启动成本(LLVM 初始化、代码生成)通常需处理数万行才能摊薄。
4.3 场景 3:频繁硬解析的 OLTP 查询
OLTP 场景中,短查询高并发执行:
- 每次执行都可能触发 JIT 编译;
- 编译线程竞争 CPU;
- 导致整体吞吐下降。
建议:OLTP 系统默认关闭 JIT。
4.4 场景 4:未正确配置阈值
若 jit_above_cost 设得太低(如 1000),大量简单查询被 JIT 编译,系统负载飙升。
五、如何诊断 JIT 是否生效?
5.1 查看执行计划
使用 EXPLAIN (ANALYZE, BUFFERS),若 JIT 生效,输出包含:
JIT:
Functions: 2
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 15.234 ms, Inlining 2.101 ms, Optimization 8.765 ms, Emission 5.432 ms
5.2 监控系统视图
sql
-- 查看 JIT 统计
SELECT * FROM pg_stat_statements
WHERE query LIKE '%your_query%';
-- 或查看全局 JIT 次数
SELECT * FROM pg_stat_database WHERE datname = current_database();
-- 字段:jit_functions, jit_generation_time 等(PG 15+)
5.3 性能对比
对同一查询分别在 jit=on/off 下运行,对比:
- 总执行时间;
- CPU 使用率(
top或perf); - I/O 等待时间。
六、常见误区
6.1 误区 1:"JIT 能加速所有查询"
错误。JIT 仅优化CPU 计算密集型部分,对 I/O、锁等待、网络无影响。
6.2 误区 2:"开启 JIT 后查询自动变快"
不一定。若未达到 jit_above_cost,JIT 不会触发;若触发但收益低,反而变慢。
6.3 误区 3:"JIT 会缓存编译结果"
不会。PostgreSQL 不缓存 JIT 代码。每次执行符合条件的查询都会重新编译(PG 16 仍如此)。
这是 JIT 在 OLTP 中不适用的主因。