3. 统计信息与基数估计:统计信息的作用、直方图、采样技术、基数估计误差

优化器做决策,靠的是什么?说白了,就是“猜”。但它不是瞎猜,它手里有一份地图——统计信息。没有统计信息,优化器就像蒙着眼开车,全凭运气。

我见过太多慢查询,根因就是统计信息过期或者压根没收集。你想想看,优化器连表里有多少行数据都不知道,它怎么选索引?怎么决定 Join 顺序?

3.1 统计信息到底存了什么?

统计信息不是玄学,它是一组实实在在的元数据。数据库在表、索引、列上维护这些数据,用来描述数据的分布特征。

统计项 说明 影响
行数(Cardinality) 表或索引中记录的总数 决定全表扫描还是索引扫描
列的唯一值数(NDV) 某列不同值的个数 影响过滤条件的选择性
列的 NULL 值数 空值数量 影响 IS NULL 条件的估算
数据分布(直方图) 值的频率分布 影响范围查询、等值查询
聚簇因子 数据在磁盘上的物理顺序与索引顺序的接近程度 影响索引回表的代价
我的习惯:每次上线新功能或批量导入数据后,第一件事就是刷新统计信息。别等慢查询报警了才想起来。

3.2 直方图——统计信息的“高清镜头”

默认情况下,优化器假设数据是均匀分布的。但现实中的数据,哪有那么规整?比如订单状态,90% 是“已完成”,5% 是“待支付”,5% 是“已取消”。如果没有直方图,优化器会以为每种状态各占三分之一,那估算出来的行数就完全跑偏了。

直方图的作用,就是告诉优化器:别天真了,数据分布长这样。

3.2.1 两种主流直方图

  • 频率直方图(Frequency Histogram):每个桶对应一个值,桶的高度就是该值的出现次数。适合唯一值少的列。
  • 等高直方图(Height-Balanced Histogram):每个桶包含大致相同数量的行,桶的边界是值。适合唯一值多的列。

我记得有一次排查一个 SQL,查询某天的订单,优化器选了全表扫描。我一看统计信息,直方图没开。加上直方图后,优化器发现那天的数据只占全表的 0.1%,果断走了索引。性能从 5 秒降到了 10 毫秒。

关键点:直方图不是越多越好。桶数太多会浪费内存,桶数太少又不够精确。一般建议桶数设为 254(Oracle 默认)或 200(SQL Server 默认),对于大部分场景够用了。

3.3 采样技术——全量统计太贵了

全表扫描收集统计信息,对于大表来说代价太高。所以数据库用了采样。采样就是在表里随机抽一部分数据,用这部分数据来推断全貌。

常见的采样方式有三种:

  1. 随机采样:随机抽取 N 行或 N% 的数据。简单,但可能漏掉小值。
  2. 系统采样:按数据块或页来采样。效率高,但如果数据分布不均匀,偏差会大。
  3. 自适应采样:先小比例采样,如果发现数据分布复杂,自动提高采样比例。这是现代数据库的趋势。

我曾经遇到一个坑:某张表有 1 亿行,DBA 为了省时间,把采样比例设成了 0.1%。结果优化器估算出来的行数偏差了 100 倍,导致执行计划选错了索引。后来我把采样比例调到 5%,问题就解决了。

避坑指南:采样比例不是越低越好。对于大表,建议至少 5%-10%。如果列上有严重的倾斜数据,建议直接全量收集,或者用增量统计信息。

3.4 基数估计误差——优化器也会“看走眼”

基数估计(Cardinality Estimation)是优化器根据统计信息,估算某个操作会返回多少行。这个估算值直接影响 Join 方式、索引选择、内存分配。

但估算嘛,总会有误差。误差的来源主要有:

  • 统计信息过期:数据变了,统计信息没更新。
  • 多列关联:优化器假设各列独立,但实际列之间有关联。比如“城市”和“邮编”,明显不是独立的。
  • 复杂谓词:比如 WHERE a LIKE '%abc%',优化器很难精确估算。
  • 采样偏差:采样数据不能代表整体分布。

误差大了会怎样?我见过一个案例:一个三表 Join 的查询,优化器估算中间结果只有 100 行,实际有 100 万行。结果它选了 Nested Loop Join,跑了 2 小时。改成 Hash Join 后,30 秒就出结果了。

我的经验:当发现执行计划中的估算行数和实际行数差了一个数量级以上,基本可以断定是基数估计出了问题。这时候先检查统计信息是否最新,再看是否需要直方图或多列统计信息。

3.5 知识体系:统计信息与基数估计全景图

下面这张图,把统计信息、直方图、采样、基数估计的关系串起来了。你可以把它当作本章的思维导图。

统计信息与基数估计知识体系 统计信息与基数估计 统计信息 直方图 采样技术 基数估计 行数 / NDV NULL值 / 聚簇因子 频率直方图 等高直方图 随机采样 系统采样 误差来源 误差影响 统计信息过期 多列关联 复杂谓词

3.6 如何减少基数估计误差?

误差不可避免,但可以控制。我总结了几个实用方法:

  • 及时更新统计信息:设置自动维护窗口,或者在数据变更超过 10% 后手动刷新。
  • 使用多列统计信息:对于经常一起出现在 WHERE 条件中的列,创建多列统计信息,让优化器知道列之间的关联。
  • 启用自适应查询优化:现代数据库(如 SQL Server 2017+、Oracle 12c+)支持运行时反馈,第一次执行错了,第二次会自动调整。
  • 手动提示(Hint):当优化器实在猜不准时,用 OPTION (HASH JOIN) 或 /*+ USE_HASH */ 强制指定执行计划。
一个小技巧:在排查慢查询时,我会先看执行计划里的“估计行数”和“实际行数”。如果两者差距很大,我会用 DBMS_STATS(Oracle)或 UPDATE STATISTICS(SQL Server)重新收集统计信息,往往立竿见影。

统计信息和基数估计,是优化器决策的基石。你喂给它什么样的统计信息,它就做出什么样的执行计划。所以,别让优化器“盲猜”——把统计信息维护好,它才能帮你选出最快的路。