前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 17|回复: 0

[开发应用] SQL Server 统计信息与基数估算治理实战:从预估 1 行实际 300 万行到增量统计与动态阈值

[复制链接]

[开发应用] SQL Server 统计信息与基数估算治理实战:从预估 1 行实际 300 万行到增量统计与动态阈值

[复制链接]
dbaai

主题

0

回帖

414

积分

DBAAI

积分
414
发表于 11 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
一、具体的问题

某业务库有张流水表 t_order_flow,按天追加,现 3.2 亿行,flow_date 上有一个非聚集索引。月初报表跑一次 4 分钟,某天忽然变成 41 分钟,没有发版、没加索引、没改 SQL,CPU 不高,磁盘队列一直顶着。

看实际执行计划,三段典型症状:


  • 「聚集索引查找 + 键查找」算子上预估 1 行、实际 3 104 872 行;
  • 嵌套循环被按「1 行驱动」排在最内层,实际执行了 310 万次;
  • 排序算子挂着黄色感叹号,Operator used tempdb to spill data,内存授予只有 1024 KB,实际需要约 260 MB。


大多数人到这一步会直接往「参数嗅探」上想,但这次不是。把 Query Store 的历史计划拉出来,每条计划里这个算子的预估值都是 1 行,一年没变过——优化器不是在猜错某个参数,而是它手里那份分布认知本身就是错的。

查统计信息:
  1. SELECT s.name AS stat_name, sp.last_updated, sp.rows,
  2.        sp.rows_sampled, sp.steps, sp.modification_counter
  3. FROM sys.stats AS s
  4. CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
  5. WHERE s.object_id = OBJECT_ID('dbo.t_order_flow');
复制代码

last_updated 停在 11 个月前,modification_counter 高达 2.9 亿。也就是说,这张表在统计信息眼里还是「一年前那 4000 万行的样子」,最近十一个月新增的数据对优化器等于不存在。

这类问题最麻烦的地方在于不报错、不告警、不写日志,只是安静地让计划一天天变差。

二、核心原理

1. 基数估算(CE)是整条计划的地基

优化器挑计划的依据是「估计成本最低」,而所有成本都建立在一个数字上:每个算子会输出多少行。这个数字叫基数估算(Cardinality Estimation)。它一错,下游全错:


  • 估 1 行 → 认为嵌套循环最便宜 → 实际循环 310 万次;
  • 估 1 行 → 认为排序只要几 KB 内存 → 只授予 1 MB → 溢出 tempdb;
  • 估 1 行 → 认为不必并行 → 单线程跑 40 分钟,16 个核闲着。


所以「优化器选错了计划」,九成应该翻译成「优化器对行数的猜测错了」。

2. 统计信息对象里装了什么

DBCC SHOW_STATISTICS('dbo.t_order_flow', 'IX_flow_date') 返回三块:

部分内容用途
头部表总行数、采样行数、更新日期、抽样百分比、步数判断这份统计有多旧、多准
直方图最多 200 步,每步记 RANGE_ROWS / EQ_ROWS / DISTINCT_RANGE_ROWS / AVG_RANGE_ROWS估算等值、范围谓词
密度向量每个索引列前缀组合的平均选择性(1/去重值数)多列等值、命中不了直方图时兜底


三个要点:


  • 直方图只有 200 步。3.2 亿行、跨 1000 个 flow_date 值,200 步意味着大量日期被合并成区间,只能按 AVG_RANGE_ROWS 平摊。分布一旦倾斜(工作日单量是周末 6 倍),平均值两头不靠。
  • 密度向量假设列独立。WHERE city='上海' AND channel='APP' 被算成「总行数 × 1/城市去重数 × 1/渠道去重数」,而现实里两者强相关,于是系统性低估。
  • 直方图右边有边界。超出最大 RANGE_HI_KEY 的谓词无步可查,只能退回平均估算——这就是自增键问题。


3. 自增键(ascending key)为什么必然估错

流水表的日期或主键几乎单调递增,今天新写入的数据永远落在直方图最右一步之外。这类值走平均逻辑:估算行数 ≈ 总行数 ÷ 该列去重值数。

全表 3.2 亿、flow_date 有 1000 个不同值,「今天」会被估成约 32 万行——还能接受。但统计信息停在 11 个月前时,去重值只有 330 天、总行数只有 4000 万,谓词直接落进密度向量的平均估算,被估成几十行。

2014 之后的新版 CE(兼容级别 120+)对超范围值有修正,但修正有前提、不保证生效,靠它兜底等于赌运气。

4. 自动更新的阈值,才是根因

AUTO_UPDATE_STATISTICS 默认开着,但「自动」不等于「及时」。触发条件是变更行数超过阈值:

版本 / 兼容级别阈值规则
≤ 120(2014 及更早行为)表行数 > 500 时:500 + 20% × 表行数
≥ 130(2016+ 行为)表行数 < 25000:500 行;≥ 25000:SQRT(1000 × 表行数),上限 20%


对 3.2 亿行的表:旧阈值要变更 3200 万行才触发,一天 30 万要等一百多天,且阈值随表变大水涨船高;新阈值 SQRT(1000 × 3.2 亿) ≈ 56.6 万,两天就能触发一次。
  1. SELECT name, compatibility_level FROM sys.databases;
复制代码

还停在 100/110/120 的库,大表统计信息基本处于「事实冻结」。另外两个隐性前提:默认按页采样(大表常常只采样几千页),且只在编译计划时顺带检查。

三、实例参考(动手步骤)

步骤 0:确认现状基线

先量化「差多少倍」,别凭感觉。计划缓存侧快速判断:
  1. SELECT TOP 10 qs.execution_count,
  2.        qs.total_elapsed_time / qs.execution_count / 1000.0 AS avg_ms,
  3.        qs.last_grant_kb,
  4.        SUBSTRING(st.text, qs.statement_start_offset / 2 + 1, 200) AS stmt
  5. FROM sys.dm_exec_query_stats AS qs
  6. CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
  7. ORDER BY qs.total_elapsed_time DESC;
复制代码

last_grant_kb 只有几百 KB、语句里又带 ORDER BY / GROUP BY 的,基本可锁定「低估 → 授予不足 → 溢出」。

步骤 1:定位到具体哪个统计对象过期
  1. SELECT OBJECT_NAME(s.object_id) AS tbl, s.name AS stat_name, s.auto_created,
  2.        sp.last_updated, sp.rows, sp.rows_sampled, sp.steps, sp.modification_counter,
  3.        CAST(sp.rows_sampled * 100.0 / NULLIF(sp.rows,0) AS DECIMAL(5,2)) AS sample_pct
  4. FROM sys.stats AS s
  5. CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
  6. WHERE OBJECTPROPERTY(s.object_id,'IsUserTable') = 1 AND sp.rows > 1000000
  7. ORDER BY sp.modification_counter DESC;
复制代码

判读三列:modification_counter 远大于 0 → 早该刷没刷;last_updated 超一个月 → 陈旧;sample_pct 个位数 → 刷过也不够准。

步骤 2:证明是统计问题,不是索引问题

把谓词值拿去和直方图对照:
  1. SELECT step_number, range_high_key, range_rows, equal_rows,
  2.        distinct_range_rows, average_range_rows
  3. FROM sys.dm_db_stats_histogram(OBJECT_ID('dbo.t_order_flow'), 2)
  4. ORDER BY step_number DESC;
复制代码

报表查 flow_date = '2026-10-31',直方图最大 range_high_key 只到 2026-09-12——谓词值根本不在描述范围里。索引没问题,问题在「地图」过期了。

步骤 3:立刻止血

先手动把当前分布拍进去,别等自动触发:
  1. UPDATE STATISTICS dbo.t_order_flow WITH FULLSCAN;             -- 全表(含列统计)
  2. UPDATE STATISTICS dbo.t_order_flow IX_flow_date WITH FULLSCAN; -- 只刷索引统计
复制代码

刷新完别急着收工:旧计划还在缓存里,不会自动变好。让这条语句重新编译来验证:
  1. SELECT ... FROM dbo.t_order_flow WHERE flow_date = '2026-10-31'
  2. OPTION (RECOMPILE);
复制代码

看到预估行数从 1 变成 30 万那一档、内存授予从 1 MB 变成 260 MB,止血成功。

步骤 4:把阈值修对,这才是根治

「每周手动刷一次」是拿人力补配置缺陷,不是方案:
  1. ALTER DATABASE YourDB SET COMPATIBILITY_LEVEL = 130;  -- 启用动态阈值
  2. DBCC TRACEON (2371, -1);                              -- 无法升级时的老库兜底
复制代码

代价必须说清:兼容级别提升会启用新版 CE,可能让一批原本恰好跑得好的语句变差。上线前把 Query Store 的 QUERY_CAPTURE_MODE 设为 ALL,跑一轮核心业务对比 avg_duration,再决定是否保留。

步骤 5:按表分级维护

超大表和分区表即使阈值修对了仍要显式维护:
  1. -- 分区表:增量统计,只刷变化分区
  2. ALTER STATISTICS st_flow_date ON dbo.t_order_flow SET INCREMENTAL = ON;
  3. -- 固定抽样比例,让准确度可复现(2016 SP1 CU4+)
  4. UPDATE STATISTICS dbo.t_order_flow IX_flow_date
  5.     WITH FULLSCAN, PERSIST_SAMPLE_PERCENT = 100;
  6. -- 多列相关性补一份统计,治组合低估
  7. CREATE STATISTICS st_city_channel ON dbo.t_order_flow (city, channel)
  8.     WITH FULLSCAN, NORECOMPUTE;
复制代码

多列统计只在同一批列组合被反复查询时才值得建;筛选统计在参数化查询里容易失效,不能滥建。

治理前后对比

指标治理前治理后
关键算子预估 / 实际1 / 3,104,87231.2 万 / 30.8 万
连接方式嵌套循环(310 万次查找)哈希匹配
内存授予1,024 KB(溢出)268 MB(无溢出)
报表 P9541 分钟3 分 10 秒
tempdb 排序溢出每轮 42 GB0
last_updated11 个月前2 天内
大表触发阈值3200 万行变更56 万行变更
人工干预每月手动刷一次无需干预


四、实操检查清单


  • [ ] 查 sys.databases.compatibility_level,低于 130 的库一律列入「统计可能冻结」清单。
  • [ ] 全库跑一次 sys.dm_db_stats_properties,按 modification_counter 降序取前 20 张表存档做基线。
  • [ ] 亿级表记录 rows_sampled / rows,低于 5% 的标记为「样本不足」。
  • [ ] 抓实际执行计划,找出「预估 / 实际」比值 > 100 或 < 0.01 的算子,逐个回溯统计对象。
  • [ ] 用 sys.dm_db_stats_histogram 确认热点谓词是否落在直方图范围内,落不进去的即为自增键盲区。
  • [ ] 改兼容级别前先抓全量 Query Store,改后对比 avg_duration 与 avg_logical_io_reads。
  • [ ] 分区表统一 INCREMENTAL = ON,并确认 sys.stats.is_incremental = 1。
  • [ ] 关键大表刷新统一带 PERSIST_SAMPLE_PERCENT,别靠抽样运气。
  • [ ] 维护作业放低峰、并发限 1,避免多个 FULLSCAN 同时打满 IO 并引发重编译风暴。
  • [ ] 治理后连续观察 5 个工作日,确认 modification_counter 回落、计划稳定,再撤掉应急的 OPTION (RECOMPILE)。


几个容易踩的坑


  • 以为「自动更新开着」就等于「统计是新的」。开着的只是机制,阈值才是开关。3 亿行表在老兼容级别下要变更 3200 万行才触发,等于实质关闭。
  • 把统计问题和参数嗅探混为一谈。区分很干脆:参数嗅探是「不同参数值计划不同、预估值随参数变」;统计陈旧是「不管什么参数,预估值都停在同一个荒谬数字」。
  • 只刷索引统计,漏掉列统计。_WA_Sys_ 开头的自动列统计同样参与估算,WHERE 用到非索引列时尤其关键。刷表时用 UPDATE STATISTICS 表名 不加限定才会连列统计一起刷。
  • 业务高峰跑全库 FULLSCAN。统计更新会让引用它的计划失效并重编译,大量并发同时重编译会打满 CPU,症状是「维护一跑业务就抖」。
  • 升了兼容级别不做回归。新版 CE 对多表连接、OR 展开、表变量的估算与旧版差异很大,不对比就上线等于拿生产做实验。
  • 拿 OPTION (RECOMPILE) 当长期方案。它只解决这一次,高频短查询每次重编译本身就有 CPU 成本。
  • 忽略分区切换。SWITCH PARTITION 搬进来的数据不会自动刷新统计,必须增量统计 + 定期刷,否则「新分区不参与估算」会周期性复发。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-11-1 17:44 , Processed in 0.020693 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表