加了索引反而更慢,优化器是不是坏了
为什么你的 SQL 明明有索引却跑得更慢?深入理解数据库优化器的“思考过程”
为什么你的 SQL 明明有索引却跑得更慢?
PostgreSQL 社区开源了一个插件 pg_plan_alternatives 可用深入理解数据库优化器的“思考过程”, 甚至能可视化整个选择过程
https://jnidzwetzki.github.io/2026/03/04/pg-plan-alternatives.html
https://github.com/jnidzwetzki/pg_plan_alternatives
PostgreSQL 优化器的一个“黑箱秘密”,99% 的开发者从没见过
数据库世界有一个非常反直觉的事实:
有时候,加了索引,SQL 反而更慢。
很多开发者第一次遇到这种情况时都会怀疑人生:
索引明明存在 SQL 也写得很简单 EXPLAIN看起来也没问题
但查询就是慢得离谱。
更离谱的是:
同一条 SQL
昨天还很快
今天突然慢了 100 倍。
如果你做数据库优化久一点,就会发现一个残酷现实:
数据库优化器,其实经常选错执行计划。
而更大的问题是:
你根本不知道它为什么选错。
最近 PostgreSQL 社区的一篇技术文章提出了一个非常有意思的工具:
pg_plan_alternatives
它第一次让我们看见:
数据库优化器的“思考过程”。
而这个过程,可能彻底改变你理解 SQL 性能的方式。
PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!
一、数据库优化器,其实是一个“选择困难症患者”
SQL 和大多数编程语言有一个根本不同。
SQL 是 声明式语言。
当你写:
SELECT * FROMusersWHERE age > 30
你只是在说:
我要这些数据。
但你完全没有告诉数据库:
要不要用索引 怎么扫描表 是否并行 是否提前过滤
这些决策全部交给数据库。
数据库需要自己决定:
如何执行这条 SQL。
例如这一条简单查询,数据库可能考虑:
1️⃣ 全表扫描(Seq Scan)
2️⃣ 索引扫描(Index Scan)
3️⃣ Bitmap Scan
4️⃣ Index Only Scan
5️⃣ 并行扫描
如果是 JOIN 查询,复杂度会瞬间爆增。
例如:
SELECT *
FROM orders
JOIN customers ON orders.cust_id = customers.id
数据库必须决定:
Join 算法
Nested Loop Hash Join Merge Join
表扫描方式
Seq Scan Index Scan Bitmap Scan
Join 顺序
orders → customers customers → orders
组合数量会迅速膨胀。
理论上复杂度接近:
O(n!)
这就是数据库优化器的工作:
在海量执行计划中,找出一个最优的。
二、数据库做决策的唯一依据:成本模型
数据库优化器本质上在做一件事情:
成本计算。
每个执行计划都会计算一个 Cost:
Cost = IO + CPU + Memory
例如 PostgreSQL 的典型成本模型:
total_cost =
seq_page_cost * pages +
cpu_tuple_cost * rows
数据库会生成多个候选执行计划:
优化器会选择:
Cost 最低的执行计划。
听起来很合理。
但这里隐藏着数据库性能问题最大的根源。
三、优化器最大的假设:统计信息是准确的
优化器所有决策都依赖一个东西:
统计信息(Statistics)
例如数据库会估算:
WHERE age > 30
可能返回:
2% rows
如果这个估算是错的:
真实情况可能是:
80% rows
优化器就会做出灾难性决策。
例如选择:
Nested Loop Join
当数据规模达到百万级:
100万 × 100万
查询直接爆炸。
数据库领域有一个非常著名的问题:
Cardinality Estimation Error
(基数估计错误)
这是数据库研究几十年的核心难题。
甚至很多顶级论文都在研究它。
因为一旦基数估计错了:
最优执行计划 ≠ 最优性能
四、EXPLAIN 有一个致命盲区
所有数据库工程师排查 SQL 性能问题时,都会用:
EXPLAIN ANALYZE
它会告诉你:
Index Scan using idx_user_age
Cost: 0.29..8.31
Rows: 10
但这里有一个巨大问题:
你只能看到最终被选择的执行计划。
你完全看不到:
数据库还考虑过哪些方案。
例如这条查询:
SELECT * FROMtestWHEREid = 10
优化器可能考虑过:
Seq Scan Index Scan Bitmap Scan Index Only Scan
但 EXPLAIN 只会显示:
Index Scan
其他方案:
全部消失。
你永远不知道:
优化器为什么没选它们 它们的成本是多少 它们是不是其实更优
这就是数据库优化的一个巨大黑箱。
五、打开黑箱:pg_plan_alternatives
最近 PostgreSQL 社区出现了一个非常酷的工具:
pg_plan_alternatives
它的核心思路非常简单:
捕获优化器生成的所有执行路径。
实现方式非常硬核:
eBPF 动态追踪。
六、eBPF:Linux 最强调试武器
eBPF 是 Linux 内核近几年最重要的技术之一。
它允许你:
attach 到任意函数 捕获运行时数据 几乎没有性能开销
Facebook、Netflix、Uber 都大量使用 eBPF 做系统观测。
而这个工具利用 eBPF:
直接 hook PostgreSQL 的一个关键函数:
add_path()
这是优化器内部最核心的函数之一。
作用是:
每生成一个候选执行计划,就调用一次。
于是工具可以捕获:
ADD_PATH: SeqScan
ADD_PATH: IndexOnlyScan
ADD_PATH: BitmapHeapScan
最后:
CREATE_PLAN: IndexOnlyScan
这意味着:
优化器其实考虑过多个方案,
但最终只选择了一个。
七、真实例子:一个看似简单的查询
例如查询:
SELECT * FROMusersWHEREid = 100
优化器可能生成这些计划:
最终选择:
Index Scan
但如果统计信息错误:
例如数据高度集中:
id=100 占 40%
成本计算会变成:
优化器会选择:
Seq Scan
于是你看到:
SQL 突然慢了几十倍。
而你完全不知道为什么。
八、真实世界事故:优化器选错计划
数据库历史上有很多著名事故。
例如:
Uber 2016 数据库事故
Uber 在一次 schema 变更后:
PostgreSQL 统计信息失效。
优化器开始大量选择:
Nested Loop Join
原本几十毫秒的查询变成:
几十秒。
数据库直接雪崩。
Shopify 数据库事故
Shopify 曾经遇到过:
统计信息失真导致:
Index Scan
变成:
Seq Scan
一个核心 API 延迟暴涨。
这些事故背后的核心原因其实一样:
优化器选错执行计划。
九、优化器其实在“剪枝”
很多人以为优化器会遍历所有执行计划。
现实不是。
因为执行计划数量太多。
例如 10 表 JOIN:
理论可能达到:
10! = 3,628,800
数据库必须做:
Plan Pruning(计划剪枝)
直接丢弃大量候选计划。
这意味着:
有些潜在更优计划:
甚至从未被考虑。
十、数据库优化的未来
过去 40 年:
数据库优化器依赖:
统计信息 成本模型 启发式规则
但最近数据库研究正在转向:
机器学习优化器
例如:
Neo optimizer Bao optimizer Kepler
核心思想是:
用真实执行数据训练优化器。
而不是依赖人工设计的成本模型。
实验显示:
在复杂查询中,
学习型优化器可以比传统优化器
快 2–10 倍。
十一、数据库高手真正看的不是执行计划
很多初级 DBA 看 SQL 性能问题:
只看一个东西:
EXPLAIN
但真正的数据库专家会问三个问题:
第一
优化器为什么选这个计划?
第二
它有没有考虑更好的计划?
第三
为什么没选?
pg_plan_alternatives
第一次让我们看到:
优化器完整的决策过程。
这就像:
以前你只看到医生开的药方,
现在你能看到:
整个会诊记录。
结语
数据库优化,从来不是简单的:
加索引
它背后其实是一个复杂系统:
统计学 搜索算法 成本模型 IO 模型 CPU Cache 操作系统
优化器做的每一个决策,
都是在 不完美信息下的最佳猜测。
而理解这些猜测,
才是数据库调优真正的开始。
参考: https://github.com/jnidzwetzki/pg_plan_alternatives