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 版本或改写查询。强制手段会掩盖根因,并可能带来编译开销。把这条链路记牢:数据分布决定统计信息,统计信息决定基数估计,基数估计决定执行计划。日常维护中,与其反复调查询提示,不如先确认统计信息是否新鲜、采样是否足够、关键列是否被覆盖。多数“计划突然变差”的问题,根因都在统计信息这一层。