Skip to content

查询优化器与慢 SQL 线上排障

代价模型、EXPLAIN (ANALYZE, BUFFERS) 深度解读、Bitmap 扫描、三种连接算法与排障手册。

Updated View as Markdown
For humans

查询优化器与慢 SQL 线上排障

校招考察频度:★ 高频(排障必考·核心生死线) 一句话速记:优化决策靠代价,SSD 必调随机代价比;计划阅读找偏差,Bitmap 转随机为有序;小表驱动走嵌套,大表哈希防落盘。

在后端校招技术面试中,面试官最看重候选人**“遇到生产接口超时或慢 SQL 时,能否冷静利用工具准确定位根因并快速优化”的实战能力。PostgreSQL 拥有关系库中最先进的代价优化器(Cost-based Optimizer, CBO)**,理解其代价模型与执行计划解读是拿下高评级的杀手锏。


一、代价优化器(CBO)核心计算模型

PostgreSQL 优化器不会盲目执行 SQL,而是会枚举所有可能的执行路径(全表扫描、索引扫描、不同 JOIN 算法与顺序),依据数学代价模型计算出总代价(Total Cost)最小的那条路径。

核心认知:执行计划中的 cost(如 cost=0.42..8.45)是没有单位的抽象开销数值,绝对不是毫秒!

关键代价参数基准(面试亮点)

  • seq_page_cost = 1.0:顺序读取一个 8KB 磁盘数据页的代价基准(作为标尺单位 1.0)。
  • random_page_cost = 4.0(生产必调项!):
    • 默认值为 4.0,源于传统机械硬盘时代,随机 I/O 寻道成本大约是顺序 I/O 的 4 倍;
    • SSD 时代致命陷阱:在如今主流的 NVMe SSD 上,随机读与顺序读的性能差距已微乎其微。若保持 4.0 不变,优化器会严重“高估”索引扫描的随机 I/O 代价,导致很多本该走索引的查询被错误判定为全表扫描(Seq Scan)!
    • 生产建议:全固态硬盘环境下强烈建议将 random_page_cost 调至 1.1 ~ 1.5。
  • cpu_tuple_cost = 0.01:CPU 处理解析一行元组的代价。

二、EXPLAIN (ANALYZE, BUFFERS) 深度阅读技巧

面试官:“你在排查慢 SQL 时,EXPLAIN 后面一般跟什么参数?怎么看懂输出?”

EXPLAIN (ANALYZE, BUFFERS, COSTS, TIMING, SUMMARY) 
SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';

为什么必须加上 ANALYZE 和 BUFFERS?

  1. 单写 EXPLAIN 只是“纸上谈兵”:仅展示优化器根据统计信息预测的估算计划,根本没有真正执行 SQL;
  2. ANALYZE 真正执行:输出真实执行耗时(actual time)与实际过滤产出的行数(actual rows);
  3. BUFFERS 揭示 I/O 真实开销:
    • shared hit=125:在内存 shared_buffers 中命中 125 个数据页,零磁盘 I/O;
    • read=48:真实从磁盘(或 OS Page Cache)读取了 48 个物理块;
    • written=3:执行过程中发生脏页刷写。

执行计划排查“四步黄金法则”

典型节点输出示例:
->  Index Scan using idx_orders_user on orders  
    (cost=0.42..8.45 rows=10 width=72) 
    (actual time=0.032..0.085 rows=12 loops=1)
    Buffers: shared hit=3
  1. 自底向上逐层阅读:从最内层的扫描节点(叶子算子)开始向外阅读数据流向;
  2. 寻找“第一处估算偏差”:重点对比估算行数 rows=10 与实际行数 actual rows=12。如果发现某一步预估只有 5 行,实际跑出来 50 万行,这就是一切慢 SQL 的万恶之源! 偏差会沿着 JOIN 树逐层几何级放大;
  3. 关注 loops 循环次数:内层算子可能被循环调用多次,算子真实的开销是 actual time × loops,真实产出行数是 actual rows × loops;
  4. 检查是否存在落盘操作:查找是否有 Sort Method: external merge Disk 或 Batches: 2(说明 work_mem 不足导致中间数据溢出到磁盘临时文件!)。

三、常见执行计划扫描节点全景

节点类型运行机制与特点调优关注点
Seq Scan(全表扫描)从磁盘顺序扫描整张表的全部数据页并过滤。小表或查询数据超过全表 30% 时完全正常;千万级大表若出现,需检查是否索引失效或缺失索引。
Index Scan(索引扫描)遍历 B-tree 索引获取 CTID,逐个回表读取 8KB 数据页验证 MVCC。适合返回少量行的高选择性查询;返回行数过多时,大量随机 I/O 回表导致性能下降。
Index Only Scan(仅索引扫描)数据全部来自覆盖索引,依赖 VM(Visibility Map)。若伴随大量 Heap Fetches,说明该表缺乏 VACUUM,VM 标记落后。
Bitmap Scan(位图扫描)PostgreSQL 特色核心利器(见下文)。先走 Bitmap Index Scan 搜集页位图,再走 Bitmap Heap Scan 按物理顺序读盘。

核心解密:Bitmap Scan 是怎么工作的?

很多同学看到 Bitmap Index Scan + Bitmap Heap Scan 感到困惑。这其实是 PostgreSQL 的一项绝技:

  • 场景:若一个查询命中 5000 条记录,直接走 Index Scan 会产生 5000 次可能无序的磁盘随机 I/O,代价很大。
  • Bitmap 的处理方式:
    1. Bitmap Index Scan:在索引中快速捞出所有满足条件的元组物理坐标(CTID);
    2. 在内存中构建一个关于数据块号(Block Number)的位图(Bitmap);
    3. 若条件包含多个索引(如 WHERE user_id = 1 AND status = 'PAID'),PG 分别对两个索引建立位图,在内存中进行极其高效的位运算(Bitwise AND / OR)!
    4. Bitmap Heap Scan:最终拿算好的位图,严格按照物理磁盘块号由小到大的顺序,一次性批量读取数据页! 成功将离散的随机 I/O 转化为顺序或局部寻道 I/O。

四、三种 JOIN 连接算法深度对比(大厂面试高频)

三种关系连接算法

连接算法核心机制最佳适用场景性能瓶颈与调优
Nested Loop外层驱动表每有一行,就去内层表中检索一次(O(M × log N))。外层结果集极小(几十行),内层表连接键建有高效索引。内层若无索引会退化为双重全表扫描(O(M × N)),直接卡死。
Hash Join先扫描小表在内存中建立 Hash 表,然后流式遍历大表探查(Probe)匹配。两表数据量均较大,且连接条件为等值比对(=),内表缺乏索引。work_mem 瓶颈:若哈希表大小超过 work_mem,哈希表将被切片溢出落盘(Batches > 1),I/O 剧增!
Merge Join要求两个输入集已经按连接键排好序,使用双指针从头到尾线性归并。两表均非常庞大,且输入本身在索引中已有序,或属于大批量范围等值连接。若输入无序,需要先执行昂贵的 Sort 排序算子。

五、生产慢 SQL 排查与调优实战手册

1. 治本之策:修复失效的统计信息

若 EXPLAIN 发现估算行数与真实行数偏差巨大,优化器就会选错计划(例如把该用 Hash Join 的选成了 Nested Loop):

-- 立即重新采样收集指定表的统计信息
ANALYZE orders;

-- 针对高基数或倾斜严重的列,调大单列统计采样精度(默认 100,最高 10000)
ALTER TABLE orders ALTER COLUMN tenant_id SET STATISTICS 500;
ANALYZE orders;

2. 生产抓取慢 SQL 神器:pg_stat_statements

在 postgresql.conf 中开启核心监控插件:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 查询系统中最耗 CPU、调用最频繁、平均耗时最长的 Top 5 慢 SQL
SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;
Navigation

Type to search…

↑↓ navigate↵ selectEsc close