3. 统计信息与基数估计

在 SQL Server 中,优化器并不直接“知道”表里有多少行满足某个条件,它依赖统计信息(Statistics)来估算中间结果集的行数,这个估算值就是基数估计(Cardinality Estimation,CE)。基数估计是成本模型的输入,而成本又决定连接顺序、连接算法(Nested Loops / Hash / Merge)、并行度、内存授予等关键决策。因此,复杂查询性能异常时,统计信息与基数估计往往是第一嫌疑对象。

统计信息的结构:直方图 + 密度向量

一个统计对象由三部分组成:头部信息(采样行数、更新日期、是否过滤统计)、直方图(Histogram)和密度向量(Density Vector)。直方图最多 200 个阶梯(Step),每个阶梯记录一个键值上界(RANGE_HI_KEY)、该值及之前值的累计行数(RANGE_ROWS)、等于该值的行数(EQ_ROWS)、相邻键值之间的行数(DISTINCT_RANGE_ROWS、AVG_RANGE_ROWS)。密度向量则记录每个前缀列组合的平均选择性,用于多列等值谓词估算。

关键点在于:直方图只覆盖首列。对于复合统计 (A, B),直方图描述 A 的分布,B 的分布只能靠密度向量近似。这就是多列谓词估算容易偏差的结构性原因。

统计信息结构:直方图阶梯与密度向量 直方图(首列,最多 200 阶梯) 键值 → 行数 EQ EQ EQ EQ EQ EQ EQ 'A' 'B' 'C' 'D' 'E' 'F' 'G' 密度向量 列组合 (A, B) 密度 0.0021 列组合 (A, B, C) 密度 0.0004 列组合 (A, B, C, D) 密度 0.00008 密度 = 1 / 不同值个数 用于多列等值谓词估算 不反映列间相关性 头部信息:采样行数、更新日期、是否过滤统计、持久化状态 直方图仅覆盖首列 → 复合统计中后续列只能靠密度向量近似 → 多列谓词估算易偏差

更新策略:自动更新、异步更新与手动维护

SQL Server 默认开启自动更新统计信息(AUTO_UPDATE_STATISTICS)。触发条件是:表中修改的行数超过阈值。SQL Server 2016 之前,阈值约为 20% 的行数;2016 及以后(兼容级别 130+)改为动态阈值:表行数小于 500 时阈值为 500 行,超过 500 后约为 sqrt(1000 × 行数)。这意味着大表在数据变化较小时也可能触发更新,但小表反而需要更多变化才触发。

自动更新有两种模式:同步更新(默认)会让查询等待统计更新完成,可能造成阻塞;异步更新(AUTO_UPDATE_STATISTICS_ASYNC ON)让查询先用旧统计执行,更新在后台进行,适合对延迟敏感但能容忍估算偏差的场景。

对于复杂查询,建议对关键大表、频繁变化的列、以及参与连接和过滤的列,采用手动更新 + 采样控制。例如:

-- 全扫描更新,适合数据分布变化大的关键表
UPDATE STATISTICS dbo.Orders WITH FULLSCAN;

-- 指定采样率,平衡精度与开销
UPDATE STATISTICS dbo.Orders WITH SAMPLE 30 PERCENT;

-- 只更新特定统计对象
UPDATE STATISTICS dbo.Orders (IX_Orders_CustomerID_OrderDate) WITH FULLSCAN;

-- 查看统计信息详情
DBCC SHOW_STATISTICS('dbo.Orders', 'IX_Orders_CustomerID_OrderDate');

判断是否需要手动更新,可以观察 DBCC SHOW_STATISTICS 输出中的“Updated”日期与“Rows Sampled”比例。若采样率过低且数据倾斜严重,估算偏差会显著放大。

基数估计器版本:CE 70 与 CE 120+

SQL Server 2014 引入了新的基数估计器(CE 120),与旧版 CE 70 在多个模型上有本质差异:

  • 指数退避(Exponential Backoff):CE 120 对多列谓词不再简单相乘,而是按列数逐步降低选择性,避免过度低估。
  • 相关性假设:CE 120 假设列间存在部分相关性,而 CE 70 假设完全独立。
  • 连接估算:CE 120 对连接基数使用更保守的模型,减少中间结果集低估。
  • NULL 与空值处理:两者对 NULL 的估算逻辑不同。

数据库兼容级别决定默认 CE 版本:兼容级别 ≤ 110 使用 CE 70,≥ 120 使用 CE 120。也可以通过 USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION') 或 QUERYTRACEON 9481 强制旧版,或用 QUERYTRACEON 2312 强制新版。实际治理中,不应全局切换 CE 版本,而应针对具体查询做 A/B 对比,确认估算偏差方向后再决定。

估计偏差对复杂查询的影响

基数估计偏差会沿着执行计划向上传播。一个中间结果集被低估 10 倍,可能导致优化器选择 Nested Loops 而非 Hash Join;被高估则可能选择 Hash Join 并申请过大内存授予,造成等待。常见偏差模式包括:

偏差模式典型原因对复杂查询的影响
低估多列谓词独立假设、参数嗅探、过期统计选择 Nested Loops,实际行数暴增,CPU 与 I/O 飙升
高估直方图阶梯过粗、采样偏差、CE 模型保守选择 Hash Join 并申请过大内存,并发下内存等待
连接顺序错误中间结果集估算错误累积大表先连接,临时结果集膨胀,Spill 到 tempdb
并行度误判估算行数低于并行阈值本可并行的查询串行执行,响应时间变长

诊断时,对比估计行数(EstimateRows)与实际行数(ActualRows)是最直接的手段。在 SSMS 中开启“包含实际执行计划”,查看每个运算符的属性;或用 SET STATISTICS XML ON 获取计划 XML,提取 EstimateRows 与 ActualRows 做比值。若某节点比值超过 10 倍,就应重点检查该节点涉及的统计信息、谓词写法和 CE 版本。

对于参数化查询,参数嗅探会导致计划复用时的估算偏差。可考虑 OPTION (RECOMPILE)、OPTION (OPTIMIZE FOR UNKNOWN) 或 OPTION (OPTIMIZE FOR @p = 值),但需评估编译开销与计划缓存影响。
不要盲目对所有表执行 FULLSCAN 更新。大表全扫描可能消耗大量 I/O 并阻塞其他查询。应优先对数据分布变化大、参与关键连接和过滤的统计对象做定向更新,并结合采样率与更新频率做权衡。

总结来说,统计信息是基数估计的输入,基数估计是成本模型的输入,成本模型决定执行计划。复杂查询性能治理中,先确认统计信息是否新鲜、直方图是否覆盖关键列、CE 版本是否匹配数据特征,再结合估计行数与实际行数的比值定位偏差节点,才能把优化落到具体运算符和具体统计对象上,而不是停留在“加索引”或“改写法”的泛泛层面。