PostgreSQL码农集散地

加了索引反而更慢,优化器是不是坏了

为什么你的 SQL 明明有索引却跑得更慢?深入理解数据库优化器的“思考过程”

为什么你的 SQL 明明有索引却跑得更慢?

PostgreSQL 社区开源了一个插件 pg_plan_alternatives 可用深入理解数据库优化器的“思考过程”, 甚至能可视化整个选择过程

https://jnidzwetzki.github.io/2026/03/04/pg-plan-alternatives.html

https://github.com/jnidzwetzki/pg_plan_alternatives


Image
pic

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  

数据库会生成多个候选执行计划:

Plan
Cost
Seq Scan
150
Index Scan
20
Bitmap Scan
40

优化器会选择:

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

优化器可能生成这些计划:

Plan
Cost
Seq Scan
120
Index Scan
5
Bitmap Scan
30

最终选择:

Index Scan  

但如果统计信息错误:

例如数据高度集中:

id=100 占 40%  

成本计算会变成:

Plan
Cost
Seq Scan
80
Index Scan
120

优化器会选择:

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