PAYATHON 2026

如何强制 SQL Server 使用近期数据样本进行查询估算?

支付老李

结论

SQL Server 不支持在基数估算时优先采样最近插入、最近更新或主键值较高的行。无论是查询级、表级还是数据库级,都没有“近期数据优先采样”这一配置。

SQL Server 通常也不会在每次编译查询时重新抽取数据样本。查询优化器使用已有的统计信息,包括直方图、密度信息和行数等。数据采样发生在创建或更新统计信息时,不是在执行每条查询时。

如果近期数据分布发生变化,导致基数估算出现偏差,可以采用这些办法:

  • 及时更新相关统计信息;
  • 对关键统计信息使用 FULLSCAN;
  • 为近期数据创建筛选统计信息;
  • 按时间分区后使用增量统计信息;
  • 处理参数嗅探和执行计划复用问题;
  • 检查查询谓词是否超出统计信息直方图的有效范围。

为什么不能让采样偏向近期数据

统计信息需要描述整个统计对象的数据分布。SQL Server 创建采样统计信息时,会从表或索引中抽取部分数据,再据此构建统计信息。系统没有提供按照插入时间、更新时间或主键大小设置采样权重的接口。

即使只采样最近的数据,得到的统计信息也可能严重误判针对历史数据的查询。优化器需要的是与具体查询谓词相匹配的数据分布,而不是单纯的“最新样本”。

统计信息直方图最多包含 200 个步骤。即使使用 FULLSCAN,也只是扫描全部数据来构建统计信息,并不表示直方图会保存每一个不同值。

首先确认哪个统计信息导致了误差

在实际执行计划中,比较各个关键算子的:

  • Estimated Number of Rows
  • Actual Number of Rows

如果两者差异很大,再检查该算子使用的连接条件、筛选条件和相关列。可以通过 DBCC SHOW_STATISTICS 查看对应的统计信息:

DBCC SHOW_STATISTICS
(
    N'dbo.Payments',
    N'IX_Payments_PaymentDate'
)
WITH STAT_HEADER, DENSITY_VECTOR, HISTOGRAM;

重点检查:

  • Updated:统计信息最后更新时间;
  • Rows 和 Rows Sampled:总行数和采样行数;
  • 直方图是否覆盖查询使用的近期值;
  • 近期数据增长后,统计信息是否仍未触发自动更新;
  • 查询条件是否涉及多个强相关列,而统计信息无法准确描述这些列之间的相关性。

也可以查询统计信息属性:

SELECT
    s.name AS statistics_name,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter
FROM sys.stats AS s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
WHERE s.object_id = OBJECT_ID(N'dbo.Payments');

更新关键统计信息

如果报表运行前刚完成大批量付款数据导入,可以在加载结束后更新相关统计信息:

UPDATE STATISTICS dbo.Payments
    IX_Payments_PaymentDate;

对于很重要且数据量可控的统计信息,可以使用全表扫描:

UPDATE STATISTICS dbo.Payments
    IX_Payments_PaymentDate
WITH FULLSCAN;

也可以指定采样比例:

UPDATE STATISTICS dbo.Payments
    IX_Payments_PaymentDate
WITH SAMPLE 50 PERCENT;

FULLSCAN 可以消除抽样误差,但不能解决所有估算问题。多列相关性、参数嗅探、表达式转换和直方图步数限制仍可能造成偏差。对大型表执行 FULLSCAN 还会消耗更多 I/O、CPU 和维护时间,因此不适合不加区分地更新整个数据库的所有统计信息。

如果希望后续更新继续使用指定的采样比例,可以设置 PERSIST_SAMPLE_PERCENT,但是否支持以及具体行为取决于 SQL Server 版本:

UPDATE STATISTICS dbo.Payments
    IX_Payments_PaymentDate
WITH SAMPLE 50 PERCENT,
     PERSIST_SAMPLE_PERCENT = ON;

这只能固定采样比例,不能让采样偏向近期行。

为近期数据创建筛选统计信息

如果报表总是查询一个相对固定的近期范围,可以创建筛选统计信息,专门描述这部分数据:

CREATE STATISTICS ST_Payments_Recent
ON dbo.Payments (PaymentDate)
WHERE PaymentDate >= '20260901';

如果查询还会按状态筛选,可以针对常用条件创建统计信息:

CREATE STATISTICS ST_Payments_Unreconciled_Date
ON dbo.Payments (PaymentDate)
WHERE ReconciliationStatus = 'Pending';

筛选统计信息比“近期优先采样”更贴合这种需求,因为它直接描述了查询关心的数据子集。

筛选条件中的日期必须是固定边界,不能直接使用 GETDATE() 等动态表达式。因此,滚动时间窗口需要通过维护任务定期重建统计信息,或者改用分区方案。查询谓词还要与筛选条件形成足够明确的匹配,优化器才更容易使用该统计信息。

对按时间增长的大表使用分区和增量统计信息

如果 Payments 表按日期分区,可以考虑启用增量统计信息。近期分区发生变化后,只需更新受影响分区的统计信息,不必频繁扫描整张历史大表。

创建增量统计信息的示例:

CREATE STATISTICS ST_Payments_PaymentDate
ON dbo.Payments (PaymentDate)
WITH INCREMENTAL = ON;

更新指定分区的示例:

UPDATE STATISTICS dbo.Payments
    ST_Payments_PaymentDate
WITH RESAMPLE ON PARTITIONS (12);

这种方式要求表采用兼容的分区结构,并且 SQL Server 版本支持增量统计信息。它解决的是大型分区表的统计信息维护成本,并不会改变采样算法,使其偏向新数据。

检查递增键和超出直方图范围的问题

日期列、标识列和持续递增的数值主键经常会遇到这种情况:新插入的值已经超过统计信息直方图记录的最大值。查询这些值时,优化器没有直接的历史分布依据,只能按照基数估算模型进行推断,结果可能明显偏低。

最直接的处理方法是在批量加载完成后更新相应统计信息:

UPDATE STATISTICS dbo.Payments
    PK_Payments
WITH FULLSCAN;

UPDATE STATISTICS dbo.Payments
    IX_Payments_PaymentDate
WITH FULLSCAN;

如果问题持续出现,还需要确认数据库兼容级别和当前使用的基数估算器。不同基数估算模型对超出直方图范围的值可能采用不同的推断方式,但不应仅为修正一条查询,就贸然更改整个数据库的兼容级别。

排除参数嗅探和计划复用问题

“偶尔出现坏计划”未必是采样造成的。如果同一查询在不同参数下返回的行数相差很大,首次编译时使用的参数可能生成只适合某类数据的执行计划。该计划随后被其他参数复用,就可能出现问题。

可以先使用 OPTION (RECOMPILE) 验证:

SELECT
    p.PaymentId,
    p.PaymentDate,
    p.Amount,
    p.ReconciliationStatus
FROM dbo.Payments AS p
WHERE p.PaymentDate >= @StartDate
  AND p.PaymentDate < @EndDate
  AND p.ReconciliationStatus = @Status
OPTION (RECOMPILE);

重新编译会让优化器根据当前参数和现有统计信息生成计划。这可能缓解参数嗅探问题,但也会增加编译成本。它适合执行频率较低、计算量较大的日报查询,不一定适合高频短查询。

还可以根据实际数据分布评估:

OPTION (OPTIMIZE FOR (@Status = 'Pending'));

或:

OPTION (OPTIMIZE FOR UNKNOWN);

这些提示都不是通用修复方案:

  • OPTIMIZE FOR 会使计划偏向指定参数;
  • OPTIMIZE FOR UNKNOWN 使用较为通用的估算,可能无法针对典型参数进行优化;
  • RECOMPILE 可以避免复用不合适的计划,但会带来额外的编译开销。

在支持 Query Store 提示的版本中,还可以通过 Query Store 为查询应用提示,而不必修改应用程序 SQL。具体能使用哪些功能,需要结合 SQL Server 版本确认。

建议的处理顺序

对于每日 reconciliation report,可以按以下顺序排查:

  1. 获取坏计划的实际执行计划,确定哪个算子出现了估算偏差。
  2. 检查相关统计信息的更新时间、采样行数和修改计数。
  3. 每日数据加载完成后,只更新与报表条件有关的统计信息。
  4. 对关键统计信息测试 FULLSCAN,确认问题是否由抽样误差造成。
  5. 如果报表只访问近期数据或特定状态的数据,考虑使用筛选统计信息或合适的复合索引。
  6. 如果表按日期分区,评估是否使用增量统计信息。
  7. 如果坏计划只是偶发,重点检查参数嗅探和计划缓存,不要只关注统计信息采样。
  8. 使用 Query Store 对比好计划和坏计划,必要时考虑计划强制或 Query Store 提示。

因此,处理方向不是配置“近期数据优先采样”,而是让统计信息覆盖查询真正关心的数据范围,并按照数据加载节奏及时维护统计信息,同时处理可能存在的参数嗅探和计划复用问题。

备注:内容仅供参考。