3. 业务负载特征分析与基线建立
数据库性能问题很少是“平均情况”的问题。真正让业务感知到卡顿、超时、报表跑不出来的,往往是每天固定时段的高峰并发、夜间批处理与报表查询之间的资源争抢。因此,性能优化的第一步不是急着改索引或调参数,而是把业务负载“看清楚”:它由哪些场景组成、各自在什么时间发生、消耗哪些资源、正常时应该是什么样子。这就是负载特征分析与性能基线建立。
一、把混合负载拆成可度量的场景
一个 SQL Server 实例上通常同时跑着多种负载,它们的特征差异极大。我们可以按“访问模式 + 时间窗口 + 资源偏好”三个维度把负载拆开:
- 在线交易(OLTP)高峰:白天 9:00–12:00、14:00–18:00,短事务、高并发、大量小范围读写,对 CPU、锁与日志写入速率敏感,单条语句通常毫秒级。
- 批处理:常在凌晨或业务低峰触发,长事务、大批量 INSERT/UPDATE/DELETE,对日志空间、tempdb、I/O 吞吐敏感,单批可能持续数分钟到数小时。
- 报表与分析查询:可能在白天被业务人员随时发起,也可能集中在月末、月初,扫描大量数据、做聚合与排序,对内存授予、tempdb 和磁盘读带宽敏感。
- 后台维护:索引重建、统计信息更新、备份、DBCC 检查,属于可控负载,但若与高峰重叠会显著放大争用。
拆分的目的是让每一类负载都有独立的度量口径。否则把 OLTP 的短事务和报表的大扫描混在一起看平均值,得到的“平均响应时间”既不能反映高峰体验,也不能定位批处理瓶颈。
二、采集哪些数据、从哪里采集
SQL Server 自带多套可用的采集入口,按“实时—短期—长期”分层使用最经济:
- 实时快照:
sys.dm_exec_requests、sys.dm_exec_sessions、sys.dm_os_wait_stats,用于抓当前正在跑的语句、等待类型和阻塞链。 - 语句级累计:
sys.dm_exec_query_stats配合sys.dm_exec_sql_text、sys.dm_exec_query_plan,可拿到每个语句的累计 CPU、逻辑读、执行次数、总耗时。 - 历史趋势:Query Store 是建立基线的核心工具,它按时间区间保存查询的执行计划、运行时统计和等待类别,能回答“上周同一时段这条语句是什么表现”。
- 性能计数器:PerfMon 或
sys.dm_os_performance_counters,采集 Batch Requests/sec、SQL Compilations/sec、Page Life Expectancy、Lock Waits/sec 等,用于判断整体压力水平。
采集频率要与负载节奏匹配:OLTP 高峰建议 15–30 秒一次快照,批处理窗口可放宽到 1–5 分钟,长期趋势用 Query Store 的 15 分钟或 1 小时聚合区间即可。采集本身也会消耗资源,务必避免在高并发时执行重量级 DMV 查询。
三、从原始数据到负载画像
采集到的数据需要按场景归类,形成可比较的画像。一个实用的做法是建立“时段 × 场景 × 指标”的矩阵。下面给出一个示例结构(数值为教学假设,非实测):
| 时段 | 主导场景 | Batch Requests/sec | 平均等待类型 | CPU 使用率 | 关键观察 |
|---|---|---|---|---|---|
| 09:00–12:00 | OLTP 高峰 | 约 1800 | LCK_M_X、PAGEIOLATCH_SH | 65%–80% | 订单写入集中,锁等待上升 |
| 14:00–18:00 | OLTP + 报表 | 约 1500 | CXPACKET、SOS_SCHEDULER_YIELD | 70%–90% | 报表并行查询抢占 CPU |
| 01:00–04:00 | 批处理 | 约 200 | WRITELOG、PAGEIOLATCH_EX | 30%–50% | 日志写入与 tempdb 压力大 |
| 08:30–09:00 | 报表集中 | 约 400 | RESOURCE_SEMAPHORE | 40%–60% | 内存授予等待明显 |
这张矩阵的价值在于:它把“什么时候慢、慢在什么等待上、由哪类负载引起”三件事绑定在一起。例如看到 14:00–18:00 的 CXPACKET 上升,就能推断是报表的并行计划与 OLTP 争抢 CPU,而不是单纯的事务锁问题。
四、建立性能基线
基线不是某一个数字,而是一组“正常范围”。建立方法建议分三步:
- 选稳定窗口:挑选连续 2–4 周、业务量相对平稳、无重大变更的时段作为参考期。
- 按场景取分位数:对每类负载的关键指标取 P50、P95、P99,而不是只看平均值。例如 OLTP 核心语句 P95 响应时间 80ms、P99 200ms,就是比“平均 30ms”更有意义的基线。
- 记录伴随条件:基线必须带上版本、索引状态、统计信息更新时间、并发用户数等上下文,否则环境一变基线就失效。
基线建立后,日常监控只需对比“当前值是否落在基线区间内”。超出 P95 或 P99 阈值时触发告警,再结合等待类型和 Query Store 回归查询定位原因。
五、识别异常模式
有了基线,异常就有了参照。常见的异常模式包括:
- 计划回归:同一语句执行次数不变,但 CPU 或逻辑读突然上升数倍,Query Store 中可见计划切换。
- 参数嗅探:首次编译用典型值生成计划,后续传入非典型参数导致扫描代替查找,表现为同一语句耗时方差极大。
- 阻塞链扩散:LCK_M_X 等待集中在少数会话,且阻塞源头是一条长事务或缺失索引的更新语句。
- 批处理窗口漂移:批处理完成时间逐日推迟,说明数据量增长或计划退化正在累积。
- 报表内存授予等待:RESOURCE_SEMAPHORE 上升,通常伴随大排序或哈希连接,需要检查内存授予与并发度。
下面这张图把“负载采集 → 场景归类 → 基线对比 → 异常识别”的完整链路和关键判断点画在一起,便于在实际排查时按图索骥。
在实际操作中,建议把上述链路固化为一个可重复的流程:每天固定时间导出 Query Store 的 Top 资源消耗查询与等待统计,按场景归档;每周对比一次基线区间,标记出偏离项;每月复核一次基线本身是否仍然代表当前业务。这样,性能优化就从“出问题再救火”转变为“用数据提前发现趋势”。