PostgreSQL 核心原理:什么场景下开启 JIT 能提升性能?(JIT 编译)

文章目录

    • [一、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:

  1. 是否为 OLTP 短查询?

    → 是:关闭 JIT

    → 否:进入下一步

  2. 是否涉及复杂表达式、宽表投影或自定义函数?

    → 否:无需 JIT

    → 是:进入下一步

  3. 数据量是否 > 10 万行?

    → 否:收益低,关闭

    → 是:开启 JIT,设置合理阈值

  4. 实测验证

    对比 jit=on/off 性能,以实际结果为准。

黄金法则:JIT 是 OLAP 的加速器,不是 OLTP 的万能药。


附录:检查 JIT 支持

sql 复制代码
-- 查看是否编译时启用 LLVM
SHOW jit_provider;
-- 若返回 llvmjit,则支持

-- 检查当前会话 JIT 状态
SHOW jit;

二、JIT 的工作原理

2.1 触发条件

JIT 并非对所有查询生效。需同时满足以下条件:

  1. jit = on(默认 off,需显式开启);
  2. 查询的估算代价 超过 jit_above_cost(默认 -1,即永不触发);
  3. 若涉及排序或哈希,则需超过 jit_optimize_above_cost(默认 -1)。

典型配置:

ini 复制代码
jit = on
jit_above_cost = 100000        # 约 10 万行扫描
jit_optimize_above_cost = 500000

2.2 编译内容

JIT 主要优化三类操作:

  1. 表达式求值(Expression Evaluation)
    a + b * c > 100upper(name) = 'ALICE'
  2. 元组去重与投影(Tuple Deforming)
    将磁盘存储的元组解析为内存结构
  3. 内联小函数(Inlining Small Functions)
    如自定义的 IMMUTABLE 函数

编译由 LLVM 后端完成(需编译时启用 --with-llvm)。

2.3 执行流程

  1. 优化器生成计划;
  2. 执行器检查总代价是否 > jit_above_cost
  3. 若是,调用 LLVM 将表达式编译为机器码;
  4. 后续行处理直接跳转至编译后的代码段;
  5. 结果与普通执行路径合并输出。

三、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 使用率(topperf);
  • 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 中不适用的主因。


相关推荐
吕司2 小时前
MySQL库的操作
数据库·mysql·oracle
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-02-05
大数据·数据库·人工智能·经验分享·搜索引擎·产品运营
逃逸线LOF2 小时前
mysql本地计算机上的MySQL服务启动后停止。某些服务在未由其他服务或程序使用时将自动停止
数据库
Remember_9932 小时前
MySQL 索引详解:从原理到实战优化
java·数据库·mysql·spring·http·adb·面试
dishugj2 小时前
【Oracle】 rac的一些问题以及解决方案
数据库·oracle
Ronin3052 小时前
日志打印和实用 Helper 工具
数据库·sqlite·rabbitmq·文件操作·uuid生成
eWidget2 小时前
面向信创环境的Oracle兼容型数据库解决方案
数据库·oracle·kingbase·数据库平替用金仓·金仓数据库
阿正的梦工坊3 小时前
使用即梦(seedream)来图生图:读取与写入飞书多维表格
数据库·飞书
Coder_Boy_3 小时前
基于SpringAI的在线考试系统-整体架构优化设计方案(续)
java·数据库·人工智能·spring boot·架构·领域驱动