一、什么是执行计划缓存

简单来说,SQL Server会把每次执行的SQL语句生成一个"执行计划",就像做菜时的菜谱一样。这个菜谱会被保存起来,下次再做同样的菜时就不用重新研究怎么做,直接照着之前的菜谱来就行。这个保存菜谱的地方就叫"执行计划缓存"。

举个例子,假设我们有个查询用户信息的SQL:

-- 技术栈:SQL Server
-- 查询用户基本信息
SELECT * FROM Users WHERE UserId = 1001

第一次执行时,SQL Server会花时间分析这个查询,决定用什么索引、怎么连接表等,生成执行计划。第二次执行同样的查询时,就直接用缓存中的计划,省去了分析的时间。

二、为什么需要关注执行计划缓存

执行计划缓存虽然好用,但也会带来一些问题。最常见的就是"计划缓存膨胀"和"参数嗅探问题"。

计划缓存膨胀就像你的手机缓存太多APP的临时文件,导致内存不足。SQL Server的内存是有限的,如果缓存了太多执行计划,真正用来存数据的内存就变少了。

参数嗅探问题更麻烦。比如下面这个查询:

-- 技术栈:SQL Server
-- 根据状态查询订单
SELECT * FROM Orders WHERE Status = @status

当@status='已完成'时,可能只有少量记录,SQL Server会生成一个适合少量数据的计划。但如果下次@status='待处理',可能有大量记录,却还在用之前的计划,导致性能急剧下降。

三、常见问题及解决方案

3.1 计划缓存占用内存过多

检查当前缓存使用情况:

-- 技术栈:SQL Server
-- 查看缓存中各类对象占用的内存
SELECT 
    objtype AS [缓存类型],
    COUNT(*) AS [计划数量],
    SUM(size_in_bytes)/1024 AS [占用内存(KB)]
FROM sys.dm_exec_cached_plans
GROUP BY objtype
ORDER BY [占用内存(KB)] DESC

解决方案:

  1. 定期清理不常用的计划:DBCC FREEPROCCACHE
  2. 针对特定计划清除:DBCC FREEPROCCACHE(plan_handle)
  3. 设置内存限制:通过sp_configure设置'max server memory'

3.2 参数嗅探导致性能问题

对于参数嗅探,我们可以用以下几种方法:

方法一:使用本地变量

-- 技术栈:SQL Server
-- 使用本地变量避免参数嗅探
DECLARE @local_status VARCHAR(20) = @status
SELECT * FROM Orders WHERE Status = @local_status

方法二:使用查询提示

-- 技术栈:SQL Server
-- 使用OPTIMIZE FOR提示
SELECT * FROM Orders WHERE Status = @status
OPTION (OPTIMIZE FOR (@status = '待处理'))

方法三:使用计划指南

-- 技术栈:SQL Server
-- 创建计划指南
EXEC sp_create_plan_guide 
    @name = N'OrderStatusGuide',
    @stmt = N'SELECT * FROM Orders WHERE Status = @status',
    @type = N'SQL',
    @module_or_batch = NULL,
    @params = N'@status varchar(20)',
    @hints = N'OPTION (OPTIMIZE FOR (@status = ''待处理''))'

四、最佳实践与注意事项

4.1 如何合理使用执行计划缓存

  1. 对于频繁执行的简单查询,缓存是好事
  2. 对于复杂查询,特别是涉及大量数据的,可以考虑禁用缓存
  3. 对于参数变化大的查询,使用上述解决方案避免参数嗅探

4.2 监控缓存健康状况

定期运行以下查询监控缓存命中率:

-- 技术栈:SQL Server
-- 查看缓存命中率
SELECT 
    CASE 
        WHEN counter_name = 'Cache Hit Ratio' THEN '缓存命中率'
        WHEN counter_name = 'Cache Pages' THEN '缓存页数'
    END AS 指标,
    cntr_value AS 值
FROM sys.dm_os_performance_counters
WHERE 
    object_name LIKE '%Plan Cache%'
    AND counter_name IN ('Cache Hit Ratio', 'Cache Pages')

健康的标准是缓存命中率通常在90%以上,如果低于这个值,可能需要调整。

4.3 其他实用技巧

  1. 使用OPTION (RECOMPILE)让关键查询每次都生成新计划:
-- 技术栈:SQL Server
-- 重要报表查询强制重新编译
SELECT * FROM Sales WHERE SaleDate BETWEEN @start AND @end
OPTION (RECOMPILE)
  1. 对于存储过程,可以在创建时指定重编译:
-- 技术栈:SQL Server
-- 创建总是重编译的存储过程
CREATE PROCEDURE GetRecentSales
    @days INT
WITH RECOMPILE
AS
BEGIN
    SELECT * FROM Sales 
    WHERE SaleDate >= DATEADD(day, -@days, GETDATE())
END

五、总结与建议

执行计划缓存是SQL Server的重要功能,用好了能显著提升性能,用不好反而会成为负担。关键是要理解它的工作原理,知道什么时候该用,什么时候不该用。

对于大多数OLTP系统,我的建议是:

  1. 保持默认设置,让SQL Server自动管理
  2. 只针对确实出现问题的查询进行优化
  3. 定期监控缓存使用情况
  4. 对关键报表查询考虑使用RECOMPILE选项
  5. 对大表上的参数化查询要特别注意参数嗅探问题

记住,没有放之四海而皆准的配置,最好的方法是通过测试找到适合自己系统的方案。