4. 统计信息与基数估计

SQL Server 的查询优化器并不真正“知道”表里有多少行满足某个条件,它依赖统计信息(Statistics)来估算。统计信息本质上是关于一个或多个列上数据分布情况的摘要,核心内容是直方图(Histogram)和密度向量(Density Vector)。优化器用这些摘要估算每个操作符的输出行数,也就是基数估计(Cardinality Estimation,CE),再据此选择连接方式、连接顺序、是否使用索引、内存授予等。可以说,统计信息是执行计划质量的“地基”。

统计信息里到底存了什么

以单列统计信息为例,直方图最多保存 200 个步长(Step)。每个步长记录一个上界值(RANGE_HI_KEY)、该区间内的行数(EQ_ROWS)、区间内不同值个数(DISTINCT_RANGE_ROWS)以及区间平均行数(AVG_RANGE_ROWS)。此外还有三个头部信息:统计信息覆盖的行数、采样行数、以及所有值的平均密度。多列统计信息只保存密度信息,不保存完整直方图,因此多列组合条件的估算精度通常低于单列。

直方图对“等值查询”和“范围查询”的估算方式不同。等值查询若命中某个 RANGE_HI_KEY,直接用 EQ_ROWS;若落在两个步长之间,则用 AVG_RANGE_ROWS 乘以区间内不同值个数。范围查询则累加区间内各步长的行数。理解这一点,就能明白为什么数据分布倾斜时,估算误差会明显放大。

统计信息如何创建

创建索引时,SQL Server 会自动为索引键列创建统计信息;对非索引列,可以用 CREATE STATISTICS 手动创建。自动创建统计信息(AUTO_CREATE_STATISTICS)默认开启,优化器在编译查询时若发现某列缺少统计信息,会自动补建。但自动创建的统计信息只针对单列,且采样率由系统决定,对复杂谓词帮助有限。

-- 手动创建带全扫描采样的多列统计信息
CREATE STATISTICS ST_Order_Customer_Date
ON Sales.Orders (CustomerID, OrderDate)
WITH FULLSCAN;

-- 查看统计信息头部与直方图
DBCC SHOW_STATISTICS ('Sales.Orders', 'ST_Order_Customer_Date');
DBCC SHOW_STATISTICS ('Sales.Orders', 'ST_Order_Customer_Date') WITH HISTOGRAM;

采样率是精度与开销的权衡。默认采样在表很大时可能只读取几万行,若数据分布不均,直方图会失真。对关键查询涉及的列,建议使用 FULLSCAN 或较高的 SAMPLE 百分比,并在业务低峰期执行。

统计信息如何更新与维护

统计信息会随数据修改而“过期”。SQL Server 通过修改计数器(Modification Counter)判断是否触发自动更新。经典阈值是:当表行数小于等于 500 时,修改 500 行触发;超过 500 行后,修改行数达到 500 + 20% × 表行数 才触发。SQL Server 2016 起引入 TF 2371 动态阈值,大表的触发比例会随行数增大而降低,但并非所有环境默认启用。

自动更新使用采样,且是同步还是异步取决于 AUTO_UPDATE_STATISTICS_ASYNC。同步更新会让查询等待统计信息重建,可能造成编译延迟;异步更新则先用旧统计信息编译,可能产生次优计划。维护策略上,常见做法是:

  • 对频繁变化的大表,在维护窗口用 UPDATE STATISTICS ... WITH FULLSCAN 定期重建;
  • 对分区表使用 UPDATE STATISTICS ... WITH RESAMPLE 或按分区更新,避免全表扫描;
  • 对关键查询列,考虑 NORECOMPUTE 之外更精细的采样控制,而不是简单关闭自动更新;
  • 用 sys.dm_db_stats_properties 查看最后更新时间和采样行数,识别长期未更新的统计信息。
-- 查看统计信息的最后更新与采样情况
SELECT s.name AS stat_name, sp.last_updated, sp.rows,
       sp.rows_sampled, sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE s.object_id = OBJECT_ID('Sales.Orders');

基数估计错误如何影响执行计划

基数估计错误本身不是“错误”,而是估算值与实际行数的偏差。偏差会沿着计划树向上传播:一个估算为 10 行的操作符实际返回 10 万行,上层连接方式、内存授予、并行度决策都会失准。典型后果包括:该用 Hash Join 却选了 Nested Loops,导致大量随机 I/O;该用广播却选了分区交换;内存授予不足导致溢出到 tempdb;或内存授予过大造成并发下降。

诊断时,先对比估计行数与实际行数。在 SSMS 中开启“包含实际执行计划”,查看每个操作符的 Estimated Number of Rows 与 Actual Number of Rows;也可用扩展事件或查询存储(Query Store)定位回归计划。若发现某列统计信息缺失或严重过期,先更新统计信息再复测;若更新后仍偏差大,说明直方图粒度不足,可考虑创建多列统计信息、使用过滤统计信息,或在 SQL Server 2014+ 上尝试不同的兼容性级别(CE 版本)来改变估算模型。

注意:不要一遇到估算偏差就强制加 OPTION (RECOMPILE) 或固定计划。应先确认统计信息是否新鲜、采样是否充分,再判断是否需要调整 CE 版本或改写查询。强制手段会掩盖根因,并可能带来编译开销。
统计信息 → 基数估计 → 执行计划 的影响链 基表数据 行数 / 分布 / 倾斜 修改计数器 统计信息 直方图 ≤200 步长 密度向量 / 采样率 基数估计 CE 估算每个操作符行数 沿计划树向上传播 执行 计划 Join/内存 自动/手动更新(阈值触发) 基数估计偏差的典型后果 • 连接方式选错:Nested Loops ↔ Hash Join • 内存授予不足 → 溢出 tempdb;授予过大 → 并发下降 • 并行度、连接顺序、索引选择偏离最优 估算偏差 诊断路径 1. 对比 Estimated vs Actual Rows → 2. 检查统计信息最后更新/采样行数 → 3. 更新统计信息后复测 4. 仍偏差大:多列/过滤统计信息、CE 版本、查询改写 → 5. 最后才考虑 RECOMPILE 或计划指南

把这条链路记牢:数据分布决定统计信息,统计信息决定基数估计,基数估计决定执行计划。日常维护中,与其反复调查询提示,不如先确认统计信息是否新鲜、采样是否足够、关键列是否被覆盖。多数“计划突然变差”的问题,根因都在统计信息这一层。