2015年,慕尼黑工业大学的研究团队做了一个实验:他们把PostgreSQL查询优化器的基数估计值全部替换成真实值,然后观察113个复杂查询的执行时间变化。

结果令人震惊——超过30%的查询性能提升了2倍以上,其中一些甚至快了100倍。这个实验后来发表在VLDB会议上,标题是"How Good Are Query Optimizers, Really?"。

这个问题的核心在于:查询优化器是数据库性能的决策中枢,但它的决策依据——成本估算——存在根本性的缺陷。这不是某个特定数据库的bug,而是困扰了整个数据库领域四十多年的结构性难题。

成本估算的三支柱

要理解优化器为什么会犯错,首先要理解它是如何工作的。

1979年,IBM的Pat Selinger在System R项目中提出了成本基数优化的框架。这个框架沿用至今,由三个核心组件构成:基数估计(预测每个操作返回多少行)、成本模型(计算每个操作的资源消耗)、计划枚举(在可能的执行计划中搜索最优解)。

其中,基数估计是最关键的一环。它是成本模型的主要输入,直接决定了优化器对不同执行计划的评价。如果基数估计错了,后续的一切计算都建立在错误的基础之上。

那么,基数估计是如何进行的?

直方图与MCV:统计信息的骨架

现代数据库普遍采用采样统计来收集基表信息。以PostgreSQL为例,ANALYZE命令会对每个表进行随机采样,构建三类统计信息:

统计类型 描述 用途
直方图(Histogram) 将列值分布划分为等频区间 范围查询的选择性估计
MCV列表(Most Common Values) 最常见值及其频率 等值查询的精确估计
Distinct Count 不同值的数量 连接操作的大小估计

对于简单的单表查询,这套机制工作得相当不错。微软SQL Server团队在2023年的研究中发现,基表选择谓词的估计中位数q-error(估计值与真实值的比值)接近1.0,意味着大多数单表估计是准确的。

但问题出现在更复杂的场景。

连接操作:错误的指数级传播

当查询涉及多个表的连接时,基数估计的准确性急剧下降。

PostgreSQL(以及大多数数据库)采用一个简单的公式来估计两个表连接后的行数:

|T1 ⋈ T2| = |T1| × |T2| / max(Distinct(T1.key), Distinct(T2.key))

这个公式基于一个关键假设:连接键的值是均匀分布的,且两个表的值域存在包含关系。

现实数据很少满足这些假设。当数据存在倾斜(skew)或相关性时,误差会快速放大。Leis等人的研究发现:

  • 对于1个连接,PostgreSQL有16%的估计误差超过10倍
  • 对于2个连接,这个比例上升到32%
  • 对于3个连接,超过一半的估计误差超过10倍

更糟糕的是,错误会沿着查询树向上传播。如果一个中间结果的基数被低估,优化器可能会选择索引嵌套循环连接,而实际上哈希连接会快得多。如果被高估,可能会选择全表扫描而非索引访问。

独立性假设:最危险的简化

为什么基数估计如此困难?根本原因在于数据库被迫做出大量简化假设,而这些假设在现实世界中经常失效。

属性独立性假设

当查询包含多个谓词时,例如:

SELECT * FROM orders 
WHERE status = 'SHIPPED' AND region = 'NORTH';

优化器需要估计同时满足两个条件的行数。在没有跨列统计信息的情况下,它只能假设两个谓词独立:

Selectivity(P1 AND P2) = Selectivity(P1) × Selectivity(P2)

如果statusregion存在相关性(比如北方地区的发货率特别高),这个假设会导致严重误差。研究发现,对于存在相关性的谓词组合,估计误差可以达到100倍甚至更高。

PostgreSQL从版本10开始引入了扩展统计信息(CREATE STATISTICS),允许用户显式声明列间的函数依赖关系。但这是一个手动过程,需要DBA识别并创建。

连接独立性假设

更棘手的是"跨连接相关性"(join-crossing correlation)。考虑这样一个查询:找出所有出生于巴黎的演员参演的法国电影。

这个查询涉及演员、城市、电影三个表,且存在隐含的相关性:出生于巴黎的演员更可能参演法国电影。但优化器对这种跨越多个表的关联一无所知,只能按独立假设进行估计。

CockroachDB的技术博客指出,这是当前基数估计研究的前沿难题——如何在合理的计算开销内捕获这种跨连接的相关性。

包含假设

连接公式中的"包含假设"(principle of inclusion)假设较小表的连接键值都能在较大表中找到匹配。当这个假设失效时(比如大量外键值在主表中不存在),连接结果会被严重高估。

NP难问题:搜索空间的困境

即使我们有完美的基数估计,优化器仍然面临另一个根本挑战:计划枚举本身是一个NP难问题

指数级爆炸的可能计划

对于一个涉及n个表的连接查询,可能的连接顺序数量随n呈指数增长。具体来说:

  • 仅考虑左深树(left-deep trees):n! 种顺序
  • 考虑所有树形(包括bushy trees):(2n-2)!/(n-1)! 种可能

一个10表连接的查询,可能的计划数量就已经是天文数字。

为了在可接受的时间内找到执行计划,数据库被迫使用动态规划(对小查询)或启发式算法(对大查询)来裁剪搜索空间。

动态规划的代价与局限

PostgreSQL对连接数小于12(可配置)的查询使用动态规划算法DPccp。该算法的时间复杂度是O(3^n),空间复杂度是O(2^n)。

当表数增加时,优化时间会急剧增长。对于超过阈值的查询,PostgreSQL会切换到遗传算法GEQO——这本质上是随机搜索,不保证找到最优解。

微软SQL Server采用Cascades框架进行自顶向下的枚举,同样面临搜索空间爆炸的问题。

成本模型:不可忽视的误差源

即使基数准确、计划完美,成本模型本身也会犯错。

磁盘与内存的鸿沟

PostgreSQL的成本模型诞生于磁盘为主的时代。其默认参数假设随机I/O比顺序I/O贵400倍,这在机械硬盘上是合理的估计。

但在内存数据库或SSD环境下,这个假设严重高估了随机访问的成本。结果?优化器可能错误地偏爱全表扫描而非索引查找。

执行引擎的隐藏开销

成本模型需要量化各种操作的开销:CPU周期、内存访问、I/O操作。但现代CPU的缓存层次、分支预测、SIMD指令使得精确建模几乎不可能。

微软SQL Server团队的实验发现,即使用真实基数进行计划选择,仍有约1%的查询出现了性能倒退——原因正是成本模型的不准确。

各数据库的差异与应对

不同数据库在处理这些问题上各有策略:

PostgreSQL

  • 扩展统计信息:支持声明函数依赖(DEPENDENCIES)和MCV组合
  • 遗传算法GEQO:处理大查询的计划枚举
  • 并行查询:部分缓解糟糕计划的影响

SQL Server

  • 新旧基数估计器:2014年重写了CE,但允许切换回旧版本
  • 自适应连接:运行时动态选择Hash Join或Nested Loops
  • Parameter Sensitive Plan优化:针对参数嗅探问题

Oracle

  • 动态采样:在优化时执行小规模查询获取实际数据分布
  • SQL Profile:存储查询的历史执行统计
  • 自适应游标共享:根据实际参数值选择不同计划

MySQL

  • 索引统计:InnoDB维护的索引基数估计
  • 索引下推:在存储引擎层提前过滤,减少回表
  • 成本模型常量:可通过系统表调整成本权重

缓解策略:当优化器不可靠时

既然优化器存在根本性局限,实践中如何应对?

统计信息的精细化管理

-- PostgreSQL: 创建扩展统计信息
CREATE STATISTICS s1 (dependencies, mcv) 
ON region, status FROM orders;

-- 手动触发统计更新
ANALYZE orders;

-- 调整采样精度
ALTER TABLE orders ALTER COLUMN status 
SET STATISTICS 500;

强制计划选择

当优化器坚持错误选择时,可以强制指定计划:

-- PostgreSQL: 强制使用特定索引
SET enable_seqscan = off;

-- SQL Server: 使用提示
SELECT * FROM orders WITH (INDEX(idx_status)) ...

-- Oracle: 使用提示
SELECT /*+ INDEX(orders idx_status) */ * FROM orders ...

计划稳定性控制

对于关键查询,固定执行计划比让优化器"自由发挥"更安全:

-- SQL Server: 使用Query Store固定计划
EXEC sp_query_store_force_plan @query_id, @plan_id;

-- Oracle: 使用SQL Plan Baseline
DECLARE
  v_plans NUMBER;
BEGIN
  v_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
    sql_id => 'abc123...'
  );
END;

学习型基数估计:未来的希望?

近年来,机器学习被引入基数估计领域。2018年,Kipf等人提出了MSCN(Multi-Set Convolutional Network),使用深度学习来估计查询基数。

这些方法的核心思路是:不再依赖简化的假设,而是从历史查询中学习数据分布的实际特征。

2022年SIGMOD上的一项系统性研究比较了各类学习方法,发现它们在某些场景下确实能显著提升估计精度。但挑战仍然存在:

  • 训练开销:需要大量历史查询数据
  • 泛化能力:对未见过的查询模式表现不稳定
  • 推理延迟:必须在毫秒级完成估计,否则影响优化时间

截至2025年,学习型基数估计仍主要处于研究阶段,尚未被主流数据库广泛采用。

结语:不完美的艺术

查询优化是数据库系统中最复杂的组件之一。四十多年来,研究人员和工程师构建了精密的框架,但始终无法摆脱核心假设与现实数据之间的张力。

理解优化器的局限性,不是为了否定它的价值——在绝大多数情况下,它确实能找到足够好的计划。而是为了在它失手时,能够诊断问题、施加干预。

下次当你发现一个本该秒级的查询跑了几分钟,不要急着质疑索引设计或硬件配置。先看看执行计划中的估计行数与实际行数——差异可能就是问题的根源。


参考资料

  1. Leis V, Gubichev A, Mirchev A, et al. How Good Are Query Optimizers, Really?. PVLDB, 9(3):204-215, 2015.
  2. Lee K, Dutt A, Narasayya V, Chaudhuri S. Analyzing the Impact of Cardinality Estimation on Execution Plans in Microsoft SQL Server. PVLDB, 16(11):2871-2883, 2023.
  3. Selinger P G, Astrahan M M, Chamberlin D D, et al. Access path selection in a relational database management system. SIGMOD, 1979.
  4. Kipf A, Kipf T, Radke B, et al. Learned Cardinalities: Estimating Correlated Joins with Deep Learning. arXiv:1809.00677, 2018.
  5. Moerkotte G, Neumann T, Steidl G. Preventing Bad Plans by Bounding the Impact of Cardinality Estimation Errors. PVLDB, 2(1):982-993, 2009.
  6. PostgreSQL Documentation: Chapter 14.2 Statistics Used by the Planner
  7. Microsoft Learn: Cardinality Estimation (SQL Server)
  8. Oracle Documentation: Understanding Optimizer Statistics
  9. CockroachDB Blog: An Introduction to Join Ordering
  10. Han Y, Wu Z, Wu P, et al. Cardinality Estimation in DBMS: A Comprehensive Benchmark Evaluation. PVLDB, 15(4):752-765, 2021.