一、为什么统计信息会"过期"?

想象一下你是个快递员,手上拿着去年的小区住户名单送快递,结果发现很多人搬走了,新住户又没登记。SQL Server的统计信息就像这份名单,当数据变化超过20%时(比如新增/删除大量记录),这个"名单"就过时了,导致查询优化器选错配送路线。

典型症状包括:

  • 同一条查询有时快如闪电,有时慢如蜗牛
  • 简单查询突然走全表扫描
  • 参数不同时执行计划天差地别
-- 示例环境:SQL Server 2019
-- 创建测试表
CREATE TABLE Orders (
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATETIME,
    Amount DECIMAL(10,2)
);

-- 插入1万条测试数据(2023年数据)
INSERT INTO Orders 
SELECT TOP 10000 
    ROW_NUMBER() OVER(ORDER BY NEWID()),
    ABS(CHECKSUM(NEWID())) % 1000,
    DATEADD(DAY, ABS(CHECKSUM(NEWID())) % 365, '2023-01-01'),
    ABS(CHECKSUM(NEWID())) % 10000 / 100.0
FROM sys.objects a CROSS JOIN sys.objects b;

-- 创建常规索引
CREATE INDEX IX_CustomerID ON Orders(CustomerID);
CREATE INDEX IX_OrderDate ON Orders(OrderDate);

二、手动更新统计信息的正确姿势

就像定期更新通讯录,我们可以用UPDATE STATISTICS命令刷新数据:

-- 更新单个表的统计信息
UPDATE STATISTICS Orders WITH FULLSCAN;
-- 使用采样更新(更快但不够精确)
UPDATE STATISTICS Orders WITH SAMPLE 50 PERCENT;

-- 更新整个数据库的统计信息
EXEC sp_updatestats;

但要注意几个细节:

  1. FULLSCAN最准确但耗资源,适合低峰期操作
  2. 采样更新速度快,但可能漏掉数据分布特征
  3. 建议对超过100万行的表使用采样更新
-- 查看统计信息最后更新时间
SELECT 
    name AS 统计信息名称,
    STATS_DATE(object_id, stats_id) AS 最后更新日期
FROM sys.stats
WHERE object_id = OBJECT_ID('Orders');

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

三、自动化更新策略配置

SQL Server其实有自动更新统计的机制,但默认阈值可能不够用。我们可以这样优化:

-- 检查数据库的自动统计更新设置
SELECT name, is_auto_update_stats_on 
FROM sys.databases;

-- 启用异步统计更新(避免阻塞查询)
ALTER DATABASE CURRENT SET AUTO_UPDATE_STATISTICS_ASYNC ON;

-- 调整自动更新阈值(默认为20%行变化)
-- 需要启用跟踪标志2371
DBCC TRACEON(2371, -1);

对于特别敏感的表,可以创建计划任务:

-- 创建定期更新作业(每周日凌晨2点)
USE msdb;
GO
EXEC dbo.sp_add_job  
    @job_name = N'Weekly_Stats_Update';
GO
EXEC sp_add_jobstep  
    @job_name = N'Weekly_Stats_Update',  
    @step_name = N'Update Statistics',  
    @subsystem = N'TSQL',  
    @command = N'EXEC sp_updatestats',   
    @database_name = N'YourDB';
GO
EXEC sp_add_jobschedule  
    @job_name = N'Weekly_Stats_Update',  
    @name = N'Weekly_Schedule',  
    @freq_type = 8, -- 每周
    @freq_interval = 1, -- 周日
    @active_start_time = 020000; -- 2:00 AM

四、高级场景处理技巧

场景1:超大型表处理

-- 对10亿级表使用增量统计
UPDATE STATISTICS Orders WITH RESAMPLE ON PARTITIONS(1);

场景2:临时表统计信息

-- 创建临时表时启用统计
SELECT * INTO #TempOrders FROM Orders 
OPTION (KEEP_PLAN, KEEPFIXED PLAN);

-- 手动更新临时表统计
UPDATE STATISTICS #TempOrders;

场景3:参数嗅探问题

-- 使用本地变量解决参数嗅探
DECLARE @CustomerID INT = 123;
SELECT * FROM Orders 
WHERE CustomerID = @CustomerID;

五、实战问题排查指南

当遇到性能波动时,按这个流程检查:

  1. 检查执行计划是否变化
-- 获取最近查询的执行计划
SELECT 
    qs.execution_count,
    qs.total_logical_reads,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qs.query_hash = 0xYourQueryHash;
  1. 比较统计信息变化
-- 对比两个时间点的统计信息
SELECT 
    stat.stats_id,
    stat.name,
    stat.auto_created,
    STATS_DATE(stat.object_id, stat.stats_id) AS stats_date
FROM sys.stats stat
WHERE stat.object_id = OBJECT_ID('Orders');
  1. 检查统计信息准确性
-- 验证统计信息与实际数据差异
SELECT 
    COUNT(*) AS ActualCount,
    STATS_DATE(OBJECT_ID('Orders'), 2) AS StatsDate
FROM Orders 
WHERE CustomerID BETWEEN 100 AND 200;

六、最佳实践总结

  1. 更新频率建议

    • OLTP系统:每周全量更新 + 关键表每日更新
    • 数据仓库:每次ETL后立即更新
  2. 监控脚本示例

-- 查找过期的统计信息
SELECT 
    OBJECT_NAME(s.object_id) AS TableName,
    s.name AS StatName,
    STATS_DATE(s.object_id, s.stats_id) AS LastUpdated,
    p.rows AS RowCount,
    p.modified_count AS RowsModified
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) p
WHERE STATS_DATE(s.object_id, s.stats_id) < DATEADD(DAY, -7, GETDATE())
ORDER BY p.modified_count DESC;
  1. 注意事项
    • 避免在业务高峰期更新大表统计
    • 超大型表建议使用分区统计
    • 临时表也需要统计信息维护
    • 监控自动更新失败的情况

记住,统计信息就像数据库的"导航地图",当地形发生变化时,及时更新地图才能保证查询走最优路径。