mcpField
S
sql-server-performance

sql-server-performance

github.com/damusix数据与数据库6250 次安装1 周前更新
代码仓库

关于 sql-server-performance

sql-server-performance 是用于诊断和调优 SQL Server 查询性能的参考 Skill。它通过 SQL 示例和判断标准,指导数据库从业者分析执行计划、等待、索引、统计信息、参数嗅探及中间表选择。

主要功能

  1. 01分析实际执行计划
  2. 02诊断等待与阻塞
  3. 03选择并调优索引策略
  4. 04检查统计信息和参数嗅探
  5. 05比较临时表、表变量与 CTE

适用场景

  1. 01调查变慢的查询
  2. 02查找锁问题或资源瓶颈成因
  3. 03根据查询访问模式调整索引
  4. 04选择中间结果的数据结构

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/damusix/skills --skill sql-server-performance

安装后在 Claude Code 中输入/sql-server-performance确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.tessl-plugin/
plugin.json
references/
batch-operations.md
execution-plans.md
index-strategy.md
locking-blocking.md
statistics-tuning.md
wait-stats.md

详细介绍

SQL Server 性能

适用场景

  • 曾经运行很快的查询现在变慢了
  • 阅读执行计划以查找瓶颈
  • 在索引策略(聚集、覆盖、筛选)之间做选择
  • 诊断锁定、阻塞或死锁
  • 编写不会导致事务日志膨胀的批量 INSERT/UPDATE/DELETE
  • 修复存储过程中的参数嗅探问题
  • 决定使用临时表还是表变量
  • 分析等待统计信息以识别资源瓶颈

不适用场景: 应用程序架构设计(表设计、命名约定、访问控制)、备份/还原策略、高可用配置或复制设置。

诊断流程图

查询变慢时,请按以下顺序操作——每一步都会缩小原因范围,再进入下一步。

步骤 1 — 获取实际执行计划

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- 然后在 SSMS 中按 Ctrl+M(包括实际执行计划)运行查询

请留意:

  • 键查找 — 先进行非聚集索引查找,再对每一行执行聚集索引查找。达到 1,000 行以上时,这会成为主要耗时。修复方法:将缺少的 SELECT 列添加到 NCI INCLUDE 列表中。
  • 运算符之间的粗箭头(粗连接线)——估算行数 ≪ 实际行数。这是基数估算失败。修复方法:更新统计信息,并检查是否存在参数嗅探。
  • Hash Match 上的黄色警告三角形——内存授予过小;哈希已溢出到 tempdb。修复方法:更新统计信息,使内存授予大小合适。
  • 大型表上的表扫描——没有聚集索引,或非 SARGable 谓词迫使 SQL Server 执行扫描。修复方法:添加聚集索引或重写谓词。
  • 索引假脱机 — SQL Server 即时构建了临时索引。修复方法:创建永久索引。

步骤 2 — 检查等待统计信息

-- 步骤 1:在问题时间段开始前创建快照
SELECT wait_type, wait_time_ms INTO #w FROM sys.dm_os_wait_stats;
-- ...等待 60 秒,或等待缓慢时段结束...
-- 步骤 2:差值显示该时间段内消耗等待时间的项目
SELECT c.wait_type, c.wait_time_ms - b.wait_time_ms AS delta_ms
FROM sys.dm_os_wait_stats c JOIN #w b ON b.wait_type = c.wait_type
WHERE c.wait_time_ms > b.wait_time_ms
  AND c.wait_type NOT IN ('SLEEP_TASK','LAZYWRITER_SLEEP','WAITFOR','CXCONSUMER')
ORDER BY delta_ms DESC;
DROP TABLE #w;
主要等待可能原因
LCK_M_*阻塞/锁定 — 请参阅 locking-blocking.md
PAGEIOLATCH_SH缺少索引或缓冲池未预热
CXPACKET并行度不均衡或 MAXDOP 过高
RESOURCE_SEMAPHORE内存授予不足 — 哈希/排序溢出
WRITELOG事务日志 I/O 瓶颈
SOS_SCHEDULER_YIELDCPU 饱和

完整方法和 DMV 查询请参阅 wait-stats.md。

步骤 3 — 评估索引

-- 缺失索引建议排名前 10(重启后会重置——请尽快处理)
SELECT TOP 10
    d.statement                 AS [Table],
    d.equality_columns,
    d.inequality_columns,
    d.included_columns,
    ROUND(s.avg_total_user_cost * s.avg_user_impact
          * (s.user_seeks + s.user_scans), 0) AS estimated_improvement
FROM sys.dm_db_missing_index_groups g
JOIN sys.dm_db_missing_index_group_stats s
    ON g.index_group_handle = s.group_handle
JOIN sys.dm_db_missing_index_details d
    ON g.index_handle = d.index_handle
WHERE d.database_id = DB_ID()
ORDER BY estimated_improvement DESC;

有关聚集键选择、覆盖索引和索引过多的权衡,请参阅 index-strategy.md。

步骤 4 — 检查统计信息是否为最新

-- 将直方图最大值与实际最大值比较(递增键问题)
DBCC SHOW_STATISTICS ('dbo.Orders', 'IX_Orders_OrderDate') WITH HISTOGRAM;
SELECT MAX(OrderDate) AS actual_max FROM dbo.Orders;
-- 如果 actual_max > last RANGE_HI_KEY,说明统计信息未涵盖最近的行
-- 检查修改计数器
SELECT s.name, sp.last_updated, sp.modification_counter,
       CAST(100.0 * sp.modification_counter / NULLIF(sp.rows, 0) AS DECIMAL(5,2))
           AS pct_modified
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE s.object_id = OBJECT_ID('dbo.Orders');

有关直方图解读和更新策略,请参阅 statistics-tuning.md。

步骤 5 — 诊断参数嗅探

-- 同一存储过程存在多个执行计划 = 嗅探不稳定
SELECT qsq.query_id, COUNT(DISTINCT qsp.plan_id) AS plan_count,
       qsqt.query_sql_text
FROM sys.query_store_query       qsq
JOIN sys.query_store_plan        qsp  ON qsp.query_id      = qsq.query_id
JOIN sys.query_store_query_text  qsqt ON qsqt.query_text_id = qsq.query_text_id
GROUP BY qsq.query_id, qsqt.query_sql_text
HAVING COUNT(DISTINCT qsp.plan_id) > 3
ORDER BY plan_count DESC;

请参阅下文的“参数嗅探”部分和 execution-plans.md。

索引策略

表应有一个经过合理选择的聚集索引,并针对辅助访问模式建立有目标性的覆盖非聚集索引。每个索引都会带来写入开销——OLTP 表的非聚集索引总数应保持在 3–5 个。

聚集索引选择——选择满足以下条件的键:

  • 窄——聚集键会复制到每个非聚集索引叶级行中。与 UNIQUEIDENTIFIER(16 字节)相比,INT(4 字节)会在所有索引和数百万行中显著减少开销。
  • 持续递增——随机插入会导致 50/50 页拆分和严重碎片。使用 INT IDENTITY、BIGINT IDENTITY 或 NEWSEQUENTIALID()。
  • 唯一——SQL Server 会静默地为重复值追加一个 4 字节的唯一化值,导致索引膨胀。
  • 静态——更新聚集键会在物理上移动行,并连带更新每个非聚集索引。

非聚集索引的列顺序:

  1. 首先放置等值谓词(col = @val)
  2. 接着放置范围谓词(col BETWEEN、col > @val)
  3. 最后放置 ORDER BY 列(如果它们在索引中跟在等值列之后,排序无需额外开销)

覆盖索引可消除键查找——将仅用于 SELECT 的列添加到 INCLUDE 中:

-- 之前:每一行都会触发一次回到聚集索引的键查找
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
    ON dbo.Orders (CustomerID);

-- 之后:覆盖索引——无需键查找
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_Covering
    ON dbo.Orders (CustomerID)
    INCLUDE (OrderDate, Status, TotalAmt);

筛选索引覆盖部分行——维护成本更低,选择性更高:

-- 仅为活动订单建立索引——比完整表的 NCI 小得多
CREATE NONCLUSTERED INDEX IX_Orders_Active
    ON dbo.Orders (CustomerID, OrderDate)
    INCLUDE (Status)
    WHERE Status = 1;

-- 对可空外部键建立唯一约束(排除 NULL)
CREATE UNIQUE NONCLUSTERED INDEX UX_Orders_ExternalRef
    ON dbo.Orders (ExternalRef)
    WHERE ExternalRef IS NOT NULL;

有关碎片阈值、填充因子指南以及缺失索引 DMV 的用法,请参阅 index-strategy.md。

SARGability

当优化器可以使用索引查找而不是扫描每一行时,谓词就是 SARGable。规则:筛选列绝不能套用函数或表达式。

非 SARGable(会强制扫描)SARGable 替代写法
WHERE YEAR(OrderDate) = 2025WHERE OrderDate >= '2025-01-01' AND OrderDate < '2026-01-01'
WHERE ISNULL(col, '') = @Val确保列为 NOT NULL;使用 WHERE col = @Val
WHERE col LIKE '%search'使用尾部通配符:WHERE col LIKE 'search%'
WHERE DATEDIFF(day, col, GETDATE()) < 30WHERE col > DATEADD(day, -30, GETDATE())
WHERE CAST(col AS VARCHAR) = '42'修正参数类型,使其与列类型匹配
WHERE LEFT(LastName, 3) = 'Smi'WHERE LastName LIKE 'Smi%'
WHERE col + 0 = @ValWHERE col = @Val(绝不要对列进行算术运算)

隐式类型转换的效果与函数相同——它会迫使优化器转换每一行。参数类型必须与列类型完全一致。在执行计划 XML 中查找 PlanAffectingConvert 警告。

参数嗅探

SQL Server 会在存储过程首次执行时,使用当时传入的实际参数值编译执行计划。该计划会被缓存并重复使用——即使后续调用传入了不同的参数。90% 的情况下,这样做是有益的;但当数据分布倾斜且被嗅探的值不具代表性时,就会产生问题。

策略排名(按顺序使用):

1. OPTIMIZE FOR UNKNOWN — 使用平均分布统计信息生成稳定的计划。当可能存在许多不同的参数值,且没有某个值占主导时,优先使用:

CREATE PROCEDURE dbo.GetOrders_ut
    @CustomerID INT
AS
BEGIN
    SET NOCOUNT ON;
    SELECT OrderID, OrderDate, TotalAmt
    FROM   dbo.Orders
    WHERE  CustomerID = @CustomerID
    OPTION (OPTIMIZE FOR (@CustomerID UNKNOWN));
END;

2. OPTION (RECOMPILE) — 每次执行都强制生成新计划,并将实际参数值嵌入计划。优化器每次都会找到最优计划,但每次调用都要付出 CPU 编译开销。适用于执行频率低但参数变化很大的存储过程(夜间报表、管理工具):

SELECT OrderID, OrderDate, TotalAmt
FROM   dbo.Orders
WHERE  CustomerID = @CustomerID
OPTION (RECOMPILE);   -- 语句级别,不是在存储过程上使用 WITH RECOMPILE

3. 复制到局部变量 — 避免使用。 将 @CustomerID 赋给局部变量会使嗅探失效,同时也会失去常见情况下嗅探带来的好处。OPTIMIZE FOR UNKNOWN 可以实现相同的统计行为,而且意图明确并有文档记录。

-- BAD: 完全禁用嗅探,即使嗅探本可以发挥作用
DECLARE @CID INT = @CustomerID;
SELECT ... WHERE CustomerID = @CID;

在启用了 Query Store 且兼容性级别为 160 的 SQL Server 2022 上,参数敏感计划优化(PSPO)可以针对倾斜分布自动维护多个计划变体。对于 SQL Server 2019 及更早版本,它不能替代上述策略。

临时表与表变量

维度临时表(#t)表变量(@t)CTE
统计信息有 — 自动创建无(2019 之前为 1 行;2019+ 延迟编译)无
索引支持所有类型仅 PRIMARY KEY、UNIQUE无
并行处理有有有
重新编译触发条件架构或统计信息变更时很少无
ROLLBACK 后仍存在否 — 会被删除是 — 内容保留否
作用域会话及被调用的存储过程仅当前批次当前语句
TempDB 缓存有 — 在存储过程中同左无

选择规则:

  • 当行数超过几百行、需要为中间结果创建索引,或需要为后续联接提供准确的基数估算时,使用临时表。
  • 当数据集较小且范围明确(1–100 行)、希望避免触发重新编译,或需要让数据在 ROLLBACK 后继续保留时,使用表变量(例如,在工作开始前插入审计日志)。
  • 当结果只被引用一次时,使用 CTE — CTE 不会被具体化;每次引用都会重新执行子查询。引用两次的 CTE 会扫描源数据两次。

SQL Server 2019 表变量延迟编译(兼容级别 150+):优化器会将使用表变量的语句延迟到表变量填充后再编译,并使用实际行数。这在很大程度上解决了 2019+ 环境中的“估算为 1 行”问题。对于列级统计信息以及非键列上的索引,临时表仍然更有优势。

-- 验证延迟编译是否已启用(兼容级别 150+)
SELECT name, value FROM sys.database_scoped_configurations
WHERE name = 'DEFERRED_COMPILATION_TV';

常见错误

错误修正方法
WHERE 子句不可 SARG切勿对筛选列使用函数 — 应对参数应用函数
高行数查询出现 Key Lookup将所需的 SELECT 列添加到 NCI 的 INCLUDE 列表中
为了“提升读取性能”而使用 NOLOCK改为启用 RCSI — 读取一致,且无共享锁开销
用局部变量副本解决参数嗅探使用 OPTIMIZE FOR UNKNOWN 或语句级 OPTION (RECOMPILE)
忽略随机键表的填充因子将填充因子设为 70–80%;按计划重建
盲目创建缺失索引建议评估写入开销及其与现有索引的重叠情况
不考虑碎片程度而重建所有索引跳过小于 1,000 页的表;跳过碎片率低于 5% 的索引
大表的自动更新统计信息始终未触发升级到兼容级别 130+,以使用动态阈值:MIN(500 + 0.20×n, SQRT(1000×n)) — 在大表上触发频率约提高 20 倍
表变量的基数固定为 1 行升级到兼容级别 150,以启用延迟编译
哈希/排序操作溢出到 tempdb修正统计信息,使内存授予大小正确;行模式 MGF(2019+)会自动调整
将 NOLOCK 当作死锁修复方案解决死锁需要锁定顺序或 RCSI — NOLOCK 无法阻止死锁
单次大批量 DELETE 导致日志膨胀在 WHILE 循环中使用 DELETE TOP (5000) 分块删除

参考文件

  • execution-plans.md — 计划读取、关键运算符、基数估算、CE 版本映射、智能查询处理(IQP)、Query Store 配置和回归检测
  • index-strategy.md — 聚集键选择、覆盖索引、筛选索引、碎片、缺失索引 DMV
  • wait-stats.md — 主要等待类型、DMV 查询、基线方法、资源关联、TempDB 配置、最大服务器内存
  • locking-blocking.md — 隔离级别、RCSI、锁升级、死锁、NOLOCK 的风险
  • batch-operations.md — 分块 DML、最小日志记录、分区切换、日志管理
  • statistics-tuning.md — 直方图解读、自动更新阈值、升序键问题、增量统计信息、筛选统计信息

评论

支持 Markdown