SQL Server 15.0.2000.5(2019 CU5)性能分析基线构建实战教程

SQL Server 15.0.2000.5(2019 CU5)性能分析基线构建实战教程

SQL Server 15.0.2000.5 是 SQL Server 2019 CU5 ,引入了 Intelligent Query Improvement(智能查询改进)、Batch Mode on Rowstore、T-SQL 增量编译等重磅特性。但要发挥这些优势,第一步永远是建立性能基线------你不知道现在的性能,就无法知道优化是否有效。本文用一篇文章讲透如何为 15.0.2000.5 构建完整的性能分析基线。


一句话总结

SQL Server 15.0.2000.5 性能基线的核心是多维度数据采集 + 历史对比 + 持续监控:通过查询存储(Query Store)+ 扩展事件(Extended Events)+ 系统视图(DMV/DMF)采集 CPU、IO、等待、执行计划等关键指标,与历史基线对比定位偏差,实现从"凭感觉调优"到"数据驱动调优"的转变。


一、为什么需要性能基线?

1.1 没有基线的困境

复制代码
❌ "数据库好像变慢了"          → 多慢?慢了多久?慢了多少?
❌ "最近查询有点卡"             → 哪个查询?比以前慢多少?
❌ "CPU 突然很高"               多高?从什么时候开始?对比基线正常值是多少?
❌ "这个月报表明天就能出完"     → 以前是不是半天就出了?为什么变慢了?

有了基线:

复制代码
✅ 当前平均响应时间 2.3s,基线 0.8s,增长 187%
✅ Top CPU 查询 #SQL_1A2B 从基线 120ms 增加到 3.2s,增长率 2566%
✅ 最近 3 天 avg_cpu_percent 从 35% 上升到 82%,偏离基线 > 2σ
✅ 该趋势与上周新增索引失效的时间点吻合

1.2 性能基线覆盖的维度

维度 采集内容 工具
CPU avg_cpu_percent、批次数、SQL 统计 sys.dm_os_performance_counters
内存 buffer cache hit ratio、page life expectancy sys.dm_os_performance_counters
IO IOPS、延迟、吞吐 sys.dm_io_virtual_file_stats
等待 类型、数量、持续时间 sys.dm_os_wait_stats
查询 执行计划、逻辑读、物理读、运行时间 Query Store、sys.dm_exec_query_stats
阻塞链、死锁、锁等待 扩展事件、sp_whoisactive
统计信息 更新时间、陈旧度 sys.dm_db_stats_properties

二、版本确认与环境检查

2.1 确认版本信息

sql 复制代码
-- 查看完整版本信息
SELECT @@VERSION;
-- 预期输出包含: Microsoft SQL Server 2019 (RTM-CU5) ... 15.0.2000.5

-- 更精确的版本号
SELECT SERVERPROPERTY('ProductVersion') AS ProductVersion,
       SERVERPROPERTY('ProductLevel') AS ProductLevel,
       SERVERPROPERTY('Edition') AS Edition,
       SERVERPROPERTY('EngineEdition') AS EngineEdition,
       SERVERPROPERTY('IsClustered') AS IsClustered,
       SERVERPROPERTY('InstanceName') AS InstanceName;

-- 列出所有累积更新热修复(CU hotfixes)
SELECT * FROM sys.dm_os_build_info;

2.2 检查兼容性级别

sql 复制代码
-- 兼容性级别影响查询优化器行为
SELECT name, compatibility_level, 
       CASE compatibility_level
         WHEN 150 THEN 'SQL Server 2019'
         WHEN 140 THEN 'SQL Server 2017'
         WHEN 130 THEN 'SQL Server 2016'
         WHEN 120 THEN 'SQL Server 2014'
       END AS CompatibilityDesc
FROM sys.databases;

15.0.2000.5 注意:SQL Server 2019 默认兼容性级别为 150,启用了智能查询改进等全部新特性。不要为了兼容降级到 140!

2.3 环境健康检查

sql 复制代码
-- 检查数据库状态
SELECT name, state_desc, recovery_model_desc, 
       page_verify_option_desc, is_auto_create_stats_on,
       is_auto_update_stats_on, is_parameterization_forced
FROM sys.databases
WHERE name NOT IN ('master','tempdb','model','msdb');

-- 检查内存状态
SELECT total_physical_memory_kb/1024/1024 AS Total_Physical_GB,
       available_physical_memory_kb/1024/1024 AS Available_Physical_GB
FROM sys.dm_os_sys_memory;

-- 检查 CPU 配置
SELECT cpu_count, hyperthread_ratio, scheduler_count
FROM sys.dm_os_sys_info;

-- 检查 IO 子系统
SELECT COUNT(DISTANCE physical_name),
       MAX(read_latency_ms) AS max_read_lat,
       MAX(write_latency_ms) AS max_write_lat
FROM sys.dm_io_virtual_file_stats(NULL, NULL);

三、建立性能计数器基线

3.1 关键性能计数器

sql 复制代码
-- =============================================
-- 采集 SQL Server 2019 CU5 关键性能计数器
-- =============================================

-- 1. CPU 相关
SELECT counter_name, cntr_value, cntr_type
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
    'SQL Compilations/sec',
    'SQL Re-Compilations/sec',
    'Batch Requests/sec',
    'SQL Attention rate',
    'Lazy Writes/sec'
);

-- 计算编译率(每秒编译数 / 每秒批处理请求数)
SELECT CAST(cntr_compilations.cntr_value AS FLOAT) / NULLIF(cntr_batches.cntr_value, 0)
       AS Compilation_Ratio
FROM sys.dm_os_performance_counters cntr_compilations
JOIN sys.dm_os_performance_counters cntr_batches
  ON cntr_compilations.counter_name = 'SQL Compilations/sec'
  AND cntr_batches.counter_name = 'Batch Requests/sec';
-- 正常值:< 0.1(软解析比例高)

-- 2. Buffer Manager
SELECT counter_name, cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
    'Buffer cache hit ratio',
    'Page life expectancy',
    'Checkpoint pages/sec',
    'Lazy writes/sec',
    'Readahead pages/sec',
    'Free list stalls/sec',
    'Stolen server memory',
    'Target server memory'
);

-- Page Life Expectancy (PLE) --- 低于 300 需要关注
SELECT CAST(cntr_value AS BIGINT) AS PLE_Seconds
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy';

-- Buffer Cache Hit Ratio --- 应接近 100
SELECT TOP 1 
  cntr_value AS Buffer_Cache_Hit_Ratio
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio';

-- 3. Memory Broker
SELECT memory_clerk_type, SUM(pages_kb)/1024/1024 AS Memory_MB
FROM sys.dm_os_memory_clerks
GROUP BY memory_clerk_type
ORDER BY 2 DESC;

-- 4. Latch 等待
SELECT latch_class, waiting_requests, wait_time_ms, 
       CAST(wait_time_ms/(NULLIF(waiting_requests,0)*1.0) AS DECIMAL(10,2)) AS Avg_Wait_ms
FROM sys.dm_os_latch_stats
WHERE waiting_requests > 0
ORDER BY wait_time_ms DESC;

-- 5. Locks
SELECT resource_type, request_mode, request_status, COUNT(*) AS Count
FROM sys.dm_tran_locks
GROUP BY resource_type, request_mode, request_status
ORDER BY Count DESC;

3.2 自动化计数器采集脚本

sql 复制代码
-- =============================================
-- 创建性能计数器采集表
-- =============================================
USE DBA_Monitor;
GO

CREATE TABLE PerfCounters_Baseline (
    CollectionTime DATETIME2(0) DEFAULT SYSUTCDATETIME(),
    CounterName NVARCHAR(512),
    CounterValue BIGINT,
    CounterType INT
);
GO

-- =============================================
-- 创建采集作业(SQL Agent Job)
-- =============================================
-- 建议每 15 分钟采集一次,保留 90 天数据
-- =============================================
DECLARE @SQL NVARCHAR(MAX) = N'
INSERT INTO DBA_Monitor.dbo.PerfCounters_Baseline (CounterName, CounterValue, CounterType)
SELECT counter_name, cntr_value, cntr_type
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
    ''Page life expectancy'',
    ''Buffer cache hit ratio'',
    ''Lazy writes/sec'',
    ''Checkpoints/sec'',
    ''SQL Compilations/sec'',
    ''SQL Re-Compilations/sec'',
    ''Batch Requests/sec'',
    ''Free list stalls/sec'',
    ''Lazy writes/sec'',
    ''Readahead pages/sec'',
    ''Active temporary databases'',
    ''Log bytes flushed/sec'',
    ''Transactions/sec''
)';

-- 使用 sp_add_jobstep 添加步骤
-- EXEC msdb.dbo.sp_add_jobstep ...

四、建立等待事件基线

4.1 采集等待统计

sql 复制代码
-- =============================================
-- SQL Server 2019 CU5 等待事件基线采集
-- =============================================

-- 1. 整体等待统计(自上次重启以来)
WITH WaitStats AS (
    SELECT 
        ws.wait_type,
        ws.waiting_tasks_count,
        CAST(ws.wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS Wait_S,
        CAST(ws.max_wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS MaxWait_S,
        CAST(ws.signal_wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS Signal_S,
        CAST((ws.wait_time_ms - ws.signal_wait_time_ms) / 1000.0 AS DECIMAL(12,2)) AS Resource_S,
        CAST(100.0 * ws.wait_time_ms / SUM(ws.wait_time_ms) OVER () AS DECIMAL(10,2)) AS Pct,
        CAST(100.0 * ws.signal_wait_time_ms / NULLIF(ws.wait_time_ms, 0) AS DECIMAL(10,2)) AS SignalPct
    FROM sys.dm_os_wait_stats ws
    WHERE ws.wait_type NOT IN (
        -- 忽略 idle/waitable 类型等待(它们代表空闲时间)
        N'BROKER_EVENTWAIT',N'BROKER_RECEIVE_WAITFOR',N'BROKER_TASK_STOP',
        N'BROKER_TO_FLUSH',N'BROKER_TRANSMITTER',N'CHECKPOINT_QUEUE',
        N'CHKPT',N'CLR_AUTO_EVENT',N'CLR_MANUAL_EVENT',N'CLR_SEMAPHORE',
        N'DBMIRROR_DBM_EVENT',N'DBMIRROR_EVENTS_QUEUE',N'DBMIRROR_WORKER_WAIT',
        N'DDR_FRAME_WORK',N'DDR_SOURCE_RECOVERY_THREAD',N'DDR_TARGET_RECOVERY_THREAD',
        N'DDR_WORKER',N'DDS_LOCK',N'DDS_NETWORK_ISSUES',
        N'KSOURCE_WAKEUP',N'LAZYWRITER_SLEEP',N'LOGMGR_QUEUE',
        N'MEMORY_ALLOCATION_EXT',N'ONDEMAND_TASK_QUEUE',
        N'PREEMPTIVE_XE_CALLBACKEXECUTE',N'PREEMPTIVE_XE_DISPATCHER',
        N'PREEMPTIVE_XE_GETTARGETSTATE',N'PREEMPTIVE_XE_SESSIONCOMMIT',
        N'PREEMPTIVE_XE_TARGETINIT',N'PREEMPTIVE_XE_TARGETFINALIZE',
        N'PBWORKER_STOPAND_WAIT',N'PNUTILS','PWAIT_ALL_COMPONENTS_INITIALIZED',
        N'QUEUEDPOSTREP_SUPPORT_THREAD',
        N'QDS_CLEANUP_STALE_TRANSTABLE',N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
        N'QDS_ASYNC_QUEUE',N'QDS_SHUTDOWN_QUEUE',
        N'REDO_THREAD_PENDING_WORK',N'REQUEST_FOR_DEADLOCK_SEARCH',
        N'RESOURCE_GATEAWAY',N'SERVER_IDLE_CHECK',N'SLEEP_BPOOL_FLUSH',
        N'SLEEP_DBSTARTUP',N'SLEEP_DCOMSTARTUP',N'SLEEP_MASTERDBREADY',
        N'SLEEP_MASTERMDREADY',N'SLEEP_MASTERUPGRADED',N'SLEEP_MSDBSTARTUP',
        N'SLEEP_SYSTEMTASK',N'SLEEP_TASK',N'SLEEP_TEMPDBSTARTUP',
        N'SNI_HTTP_ACCEPT',N'SOS_WORKDispatcher',N'SP_SERVER_DIAGNOSTICS_SLEEP',
        N'SQLTRACE_BUFFER_FLUSH',N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
        N'SQLTRACE_WAIT_ENTRIES',N'STARTUP_DEPENDENCY_MANAGER',
        N'TASK_EXELOOP',N'TEAMMER_COLLECTOR_SUSPEND',N'TEAMMER_JOB_SUSPEND',
        N'TEAMMER_WORKER_SUSPEND',N'TRACEWRITE',
        N'WTI_CLIENT_FAILOVER_LISTENER_LISTEN',
        N'XE_DISPATCHER_WAIT',N'XE_LIVE_TARGET_TVF',N'XE_TIMER_EVENT'
    )
)
SELECT 
    ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS RowNum,
    wait_type AS WaitingEventType,
    waiting_tasks_count AS EventCount,
    Wait_S AS SecondsWaiting,
    MaxWait_S AS MaxSecondsPerEvent,
    Resource_S AS ResourceTime_S,
    Signal_S AS SignalTime_S,
    Pct,
    SignalPct,
    CASE 
        WHEN wait_type LIKE N'LCK%' THEN 'Lock/Latch'
        WHEN wait_type LIKE N'PAGE%LATCH%' THEN 'Latch'
        WHEN wait_type LIKE N'PAGEIO_LATCH%' OR wait_type LIKE N'PAGELATCH%' THEN 'Latch'
        WHEN wait_type LIKE N'ASYNC_%' OR wait_type LIKE N'BMK%' THEN 'Background'
        WHEN wait_type IN (N'CXPACKET',N'CMEMTHREAD',N'CXCONSUMER') THEN 'Parallelism'
        WHEN wait_type LIKE N'WRITELOG' OR wait_type LIKE N'LOGBUFFER%' THEN 'LogWriter'
        WHEN wait_type LIKE N'PAGEIOLATCH%' THEN 'DiskIO'
        ELSE 'Other'
    END AS Category
FROM WaitStats
WHERE waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;

4.2 关键等待事件解读(15.0.2000.5 环境)

等待类型 含义 正常阈值 优化方向
PAGEIOLATCH_SH/EX 数据页读取到内存等待 < 10% 加内存、改善 IO
PAGEIOLATCH_DT 脏页写入磁盘等待 < 5% 改善 IO 子系统
WRITELOG Redo 日志写入等待 < 10% 优化事务、RAID10、独立日志盘
CXPACKET 并行查询协调等待 < 25% 调整 MAXDOP、优化导致并行的查询
*LCK_M_系列 锁等待 < 5% 优化事务隔离、减少锁粒度
SOS_SCHEDULER_YIELD CPU 调度器让步 < 25% CPU 过载,优化 SQL
RESOURCE_SEMAPHORE 内存查询 grant 等待 < 5% 调整 max server memory、优化 SQL
THREADPOOL 线程池耗尽 任何 > 0 需立即关注 增加 worker_threads、排查长时间运行查询

4.3 基线存储与趋势分析

sql 复制代码
-- 创建等待统计基线表
USE DBA_Monitor;
GO

CREATE TABLE WaitStats_Baseline (
    CollectionTime DATETIME2(0) DEFAULT SYSUTCDATETIME(),
    InstanceName SYSNAME DEFAULT @@SERVERNAME,
    WaitType NVARCHAR(60),
    WaitingTasksCount BIGINT,
    WaitTimeMS BIGINT,
    MaxWaitTimeMS BIGINT,
    SignalWaitMS BIGINT
);
GO

-- 采样采集存储过程
CREATE OR ALTER PROCEDURE CollectWaitStatsSnapshot
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SampleTime DATETIME2(0) = SYSUTCDATETIME();
    
    -- 插入快照数据(累计值,供后续差分计算)
    INSERT INTO DBA_Monitor.dbo.WaitStats_Baseline (
        CollectionTime, InstanceName, WaitType,
        WaitingTasksCount, WaitTimeMS, MaxWaitTimeMS, SignalWaitMS
    )
    SELECT 
        @SampleTime,
        @@SERVERNAME,
        ws.wait_type,
        ws.waiting_tasks_count,
        ws.wait_time_ms,
        ws.max_wait_time_ms,
        ws.signal_wait_time_ms
    FROM sys.dm_os_wait_stats ws
    WHERE ws.wait_type NOT IN (/* 同上忽略列表 */);
END;
GO

-- 趋势分析查询(对比最近7天和基线平均值)
SELECT 
    wb.WaitType,
    AVG(wb.WaitTimeMS) AS Baseline_Avg_PerMin,
    MAX(wb.WaitTimeMS) AS Baseline_Max_PerMin,
    -- 最近7天平均
    (SELECT AVG(WaitTimeMS) FROM DBA_Monitor.dbo.WaitStats_Baseline wbi 
     WHERE wbi.WaitType = wb.WaitType 
     AND wbi.CollectionTime >= DATEADD(DAY, -7, @CurrentTime)) AS Last7Day_Avg_PerMin,
    -- 偏离度
    CAST((
        (SELECT AVG(WaitTimeMS) FROM DBA_Monitor.dbo.WaitStats_Baseline wbi 
         WHERE wbi.WaitType = wb.WaitType 
         AND wbi.CollectionTime >= DATEADD(DAY, -7, @CurrentTime)) 
        / NULLIF(AVG(wb.WaitTimeMS), 0)
    ) * 100 AS DECIMAL(5,1)) AS DeviationFromBaseline_Pct
FROM DBA_Monitor.dbo.WaitStats_Baseline wb
WHERE wb.CollectionTime >= DATEADD(DAY, -90, @CurrentTime)
GROUP BY wb.WaitType
HAVING SUM(CASE WHEN wb.CollectionTime >= DATEADD(DAY, -7, @CurrentTime) THEN 1 ELSE 0 END) > 0
   AND SUM(CASE WHEN wb.CollectionTime < DATEADD(DAY, -7, @CurrentTime) THEN 1 ELSE 0 END) > 0
ORDER BY DeviationFromBaseline_Pct DESC;

五、查询存储(Query Store)基线

5.1 为什么用 Query Store 做基线?

SQL Server 2016+ 引入 Query Store,15.0.2000.5 中进一步增强。它是最适合做查询性能基线的工具,因为它自动记录每个查询的执行计划、运行时统计和性能指标,支持按时间对比。

5.2 启用并配置 Query Store

sql 复制代码
-- =============================================
-- 为业务数据库启用 Query Store(以 OLTP 为例)
-- =============================================
ALTER DATABASE OLTP
SET QUERY_STORE = ON;

ALTER DATABASE OLTP
SET QUERY_STORE (
    OPERATION_MODE = READ_WRITE,              -- 采集模式
    CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 90),  -- 数据保留天数
    DATA_FLUSH_INTERVAL_SECONDS = 900,         -- 每 15 分钟刷新一次
    INTERVAL_LENGTH_MINUTES = 60,              -- 按小时聚合
    MAX_STORAGE_SIZE_MB = 512,                 -- 最大存储
    SIZE_BASED_CLEANUP_MODE = AUTO,            -- 自动清理
    QUERY_CAPTURE_MODE = AUTO,                 -- 自动捕获(可改 ALL 或 NONE)
    MAX_PLANS_PER_QUERY = 200,                -- 单查询最多存储计划数
    WAIT_STATS_CAPTURE_MODE = ON               -- 同时捕获等待统计
);

-- 验证
SELECT name, actual_state, actual_state_desc, desired_state, desired_state_desc,
       readonly_reason, current_storage_size_mb, max_storage_size_mb,
       query_capture_mode_desc, stale_query_threshold_days
FROM sys.database_query_store_config;

5.3 查询基线中的异常查询

sql 复制代码
-- =============================================
-- 查询存储基线分析查询
-- =============================================
USE OLTP;
GO

-- 1. Top 最耗时间的查询(对比两个时间段)
-- 查找最近 7 天比之前 7 天平均响应时间增加最多的查询
DECLARE @StartBaseline DATETIME2 = DATEADD(WEEK, -2, SYSUTDGETIME());
DECLARE @EndBaseline DATETIME2 = DATEADD(WEEK, -1, SYSUTDGETIME());
DECLARE @StartCurrent DATETIME2 = DATEADD(WEEK, -1, SYSUTDGETIME());
DECLARE @EndCurrent DATETIME2 = SYSUTDGETIME();

-- 最近 vs 之前:查询平均 duration 变化最大的前 20 条
SELECT TOP 20
    qsq.query_id,
    qsqt.query_sql_text AS QueryText,
    -- 之前基线平均
    AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline 
         THEN qrm.avg_logical_io_reads END) AS Baseline_AvgReads,
    -- 当前平均
    AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent 
         THEN qrm.avg_logical_io_reads END) AS Current_AvgReads,
    -- 之前基线平均 duration(ms)
    AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline 
         THEN qrm.avg_duration END) AS Baseline_AvgDuration_MS,
    -- 当前平均 duration(ms)
    AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent 
         THEN qrm.avg_duration END) AS Current_AvgDuration_MS,
    -- 执行次数变化
    SUM(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline 
         THEN qrm.count_executions ELSE 0 END) AS Baseline_ExecCount,
    SUM(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent 
         THEN qrm.count_executions ELSE 0 END) AS Current_ExecCount
FROM sys.query_store_runtime_metric_range qmr
JOIN sys.query_store_runtime_metric qrm ON qmr.metric_id = qrm.metric_id
                                         AND qmr.query_runtime_metric_range_id = qmr.query_runtime_metric_range_id
JOIN sys.query_store_query qsq ON qrm.query_id = qsq.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qmr.metric_id IN (1, 2)  -- 1 = Duration, 2 = LogicalIOReads
  AND qmr.range_start_time >= @StartBaseline
  AND qmr.range_end_time <= @EndCurrent
  AND qsq.is_internal_object = 0
GROUP BY qsq.query_id, qsqt.query_sql_text
HAVING SUM(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent 
                THEN qrm.count_executions ELSE 0 END) > 0
ORDER BY 
    (AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent 
              THEN qrm.avg_duration END) 
     - AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline 
               THEN qrm.avg_duration END)) DESC;

5.4 检测执行计划回归

sql 复制代码
-- 查找同一查询有多个执行计划且性能退化的
SELECT 
    qsqt.query_sql_text,
    qsq.query_id,
    qsp.plan_id,
    qsp.is_compatible_plan,
    qsp.last_execution_time,
    qsp.avg_logical_io_reads,
    qsp.avg_duration,
    qsp.count_executions,
    qsp.avg_cpu_time,
    qsp.first_compiler_version,
    CASE 
        WHEN qsp.first_compiler_version LIKE '15.0.2000%' THEN 'SQL Server 2019 CU5'
        WHEN qsp.first_compiler_version LIKE '15.0.1%' THEN 'SQL Server 2019 RTM-CU1'
        ELSE qsp.first_compiler_version
    END AS CompilerVersion
FROM sys.query_store_plan qsp
JOIN sys.query_store_query qsq ON qsp.query_id = qsq.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qsq.is_internal_object = 0
ORDER BY qsp.avg_duration DESC;

-- 强制使用已知良好的执行计划(如果检测到退化)
-- EXEC sp_query_store_force_plan @query_id = XX, @plan_id = YY;
-- EXEC sp_query_store_unforce_plan @plan_id = YY;

六、执行计划基线

6.1 采集当前活跃查询的执行计划

sql 复制代码
-- =============================================
-- 实时查询活动监控(快照)
-- =============================================
SELECT 
    req.session_id,
    req.blocking_session_id,
    EST.text AS QueryText,
    req.status,
    SUBSTRING(EST.text, (req.statement_start_offset/2)+1, 
        ((CASE req.statement_end_offset 
           WHEN -1 THEN DATALENGTH(EST.text) 
           ELSE req.statement_end_offset 
         END - req.statement_start_offset)/2)+1) AS StatementText,
    DATEDIFF(SECOND, req.start_time, GETDATE()) AS Elapsed_Sec,
    req.cpu_time,
    req.total_elapsed_time,
    req.logical_reads,
    req.reads,
    req.writes,
    req.granted_query_memory_pages,
    req.wait_type,
    req.wait_time,
    req.last_wait_type,
    r.plan_handle,
    qp.query_plan AS XMLExecutionPlan,
    DB_NAME(req.database_id) AS DatabaseName,
    t.task_alloc_mb,
    t.task_dealloc_mb,
    ses.program_name,
    ses.host_name,
    ses.login_name
FROM sys.dm_exec_requests req
CROSS APPLY sys.dm_exec_sql_text(req.sql_handle) EST
OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) qp
LEFT JOIN (
    SELECT session_id,
           SUM(user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8 / 1024 AS task_alloc_mb,
           SUM(internal_objects_alloc_page_count - internal_objects_dealloc_page_count) * 8 / 1024 AS task_dealloc_mb
    FROM sys.dm_db_task_space_usage
    GROUP BY session_id
) t ON req.session_id = t.session_id
JOIN sys.dm_exec_sessions ses ON req.session_id = ses.session_id
WHERE req.session_id > 50  -- 排除系统会话
  AND req.command NOT IN ('BACKGROUND', 'LAZY WRITER', 'CHECKPOINT')
ORDER BY req.cpu_time DESC, req.total_elapsed_time DESC;

6.2 长期执行计划统计基线

sql 复制代码
-- =============================================
-- 查询性能统计基线(基于 dm_exec_query_stats)
-- 这是最核心的性能基线数据来源
-- =============================================

-- Top 20 最耗资源的 SQL(按平均 CPU 时间)
SELECT TOP 20
    sql_txt.text AS SQL_Text,
    qt.query_plan AS Execution_Plan,
    qs.creation_time AS Plan_Created,
    qs.last_execution_time AS Last_Run,
    qs.execution_count AS Execute_Count,
    CAST((qs.total_elapsed_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)) AS Avg_Elapsed_Sec,
    CAST((qs.total_worker_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)) AS Avg_CPU_Sec,
    CAST((qs.total_elapsed_time / 1000000.0) AS DECIMAL(12,2)) AS Total_Elapsed_Sec,
    CAST((qs.total_worker_time / 1000000.0) AS DECIMAL(12,2)) AS Total_CPU_Sec,
    qs.min_worker_time / 1000000.0 AS Min_CPU_Sec,
    qs.max_worker_time / 1000000.0 AS Max_CPU_Sec,
    qs.min_elapsed_time / 1000000.0 AS Min_Elapsed_Sec,
    qs.max_elapsed_time / 1000000.0 AS Max_Elapsed_Sec,
    qs.min_granted_query_memory * 8 / 1024 AS Min_Grant_MB,
    qs.max_granted_query_memory * 8 / 1024 AS Max_Grant_MB,
    qs.min_rows,
    qs.max_rows,
    qs.total_logical_reads / NULLIF(qs.execution_count, 0) AS Avg_Logical_Reads,
    qs.total_physical_reads AS Total_Physical_Reads,
    qs.total_logical_writes AS Total_Logical_Writes,
    qs.plan_handle
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) sql_txt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qt
WHERE sql_txt.dbid > 4  -- 排除系统库
  AND sql_txt.text NOT LIKE '%sys.%'
  AND sql_txt.text NOT LIKE '%dm_exec%'
ORDER BY Avg_CPU_Sec DESC;

七、扩展事件(Extended Events)高性能采集

7.1 为什么用扩展事件而不是 Profiler?

SQL Server Profiler 已被标记为弃用,扩展事件是 15.0.2000.5 的性能跟踪首选。开销小(1-3% vs Profiler 的 10-30%),可以长期运行采集生产环境性能数据。

7.2 创建性能监控会话

sql 复制代码
-- =============================================
-- 创建长期运行的性能监控扩展事件会话
-- 监控慢查询(> 1 秒)、死锁、阻塞、编译重编译
-- =============================================

-- 1. 创建会话
IF EXISTS (SELECT * FROM sys.server_event_sessions WHERE name = 'PerfMonitor')
    DROP EVENT SESSION PerfMonitor ON SERVER;
GO

CREATE EVENT SESSION PerfMonitor ON SERVER
-- 追踪慢查询
ADD EVENT sqlserver.rpc_completed(
    ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name, 
           sqlserver.nt_username, sqlserver.username, sqlserver.session_id, sqlserver.sql_text)
    WHERE duration > 1000000                              -- > 1 秒
      AND database_name <> 'master'                       -- 排除 master
      AND database_name <> 'tempdb'
      AND sqlserver.username <> 'sa'                      -- 排除 sa
),
ADD EVENT sqlserver.sql_batch_completed(
    ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name, 
           sqlserver.nt_username, sqlserver.username, sqlserver.session_id, sqlserver.sql_text)
    WHERE duration > 1000000                              -- > 1 秒
      AND database_name <> 'master'
      AND database_name <> 'tempdb'
),
-- 追踪死锁
ADD EVENT sqlserver.xml_deadlock_report(
    ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name)
),
-- 追踪阻塞
ADD EVENT sqlos.wait_info(
    ACTION(sqlserver.database_name, sqlserver.session_id, sqlserver.sql_text)
    WHERE wait_type IN (
        N'LCK_M_*', N'OLEDB', N'WSOCK', N'SOS Scheduler Yield',
        N'CXPACKET', N'CXCONSUMER', N'CXFETCH BY ROWS'
    )
    AND duration > 5000                                   -- > 5 秒
),
-- 追踪资源告警
ADD EVENT sqlserver.error_reported(
    ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name, sqlserver.sql_text)
    WHERE severity >= 17                                  -- 严重性 >= 17(潜在硬件问题)
),
-- 追踪编译和重编译
ADD EVENT sqlserver.sp_statement_recompile(
    ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.sql_statement_recompile(
    ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.hash_warning(
    ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.sort_warnings(
    ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.auto_stats(
    ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.username)
),
-- SPID 事件(用于关联查询和执行)
ADD EVENT sqlserver.module_end(
    ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.username, 
           sqlserver.session_nt_username, sqlserver.client_app, sqlserver.client_hostname,
           sqlserver.plan_handle, sqlserver.tsql_stack)
    WHERE duration > 100000                               -- > 100ms
),
ADD EVENT sqlserver.rpc_completed(
    ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.plan_handle)
    WHERE duration > 100000
),
ADD EVENT sqlserver.sql_batch_completed(
    ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.plan_handle)
    WHERE duration > 100000
);
GO

-- 2. 启动会话
ALTER EVENT SESSION PerfMonitor ON SERVER STATE = START;
GO

7.3 从扩展事件读取数据

sql 复制代码
-- =============================================
-- 查询扩展事件数据
-- =============================================

-- 读取慢查询事件
SELECT 
    xe.event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS EventTime,
    xe.event_data.value('(event/action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS DatabaseName,
    xe.event_data.value('(event/action[@name="client_app"]/value)[1]', 'NVARCHAR(128)') AS ClientApp,
    xe.event_data.value('(event/action[@name="client_hostname"]/value)[1]', 'NVARCHAR(128)') AS ClientHost,
    xe.event_data.value('(event/action[@name="nt_username"]/value)[1]', 'NVARCHAR(128)') AS Username,
    xe.event_data.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000000.0 AS Duration_Seconds,
    xe.event_data.value('(event/data[@name="cpu_time"]/value)[1]', 'BIGINT') / 1000000.0 AS CPU_Seconds,
    xe.event_data.value('(event/data[@name="logical_reads"]/value)[1]', 'BIGINT') AS LogicalReads,
    xe.event_data.value('(event/data[@name="row_count"]/value)[1]', 'BIGINT') AS RowsReturned,
    xe.event_data.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)') AS SqlText,
    xe.event_data.value('(event/data[@name="object_name"]/value)[1]', 'NVARCHAR(256)') AS ObjectName,
    xe.event_data.value('(event/action[@name="plan_handle"]/value)[1]', 'VARBINARY(64)') AS PlanHandle
FROM sys.fn_xe_file_target_read_file(N'PerfMonitor*.xel', NULL, NULL, NULL) xe
CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) casted
ORDER BY EventTime DESC;

八、数据库引擎核心指标基线

8.1 采集核心运行时指标

sql 复制代码
-- =============================================
-- 数据库引擎核心指标基线(一次性快照)
-- 建议在业务高峰期采集(如上午 10:00-11:00)
-- =============================================

-- 1. 缓冲区命中率
SELECT 
    cntr_value AS BufferCacheHitRatio_Percent
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio'
  AND instance_name = '_Total';

-- 2. 页面寿命期望
SELECT 
    cntr_value AS PageLifeExpectancy_Seconds
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
  AND instance_name = '_Total';

-- 3. 可用目标内存
SELECT 
    ROUND(available_target_memory_kb / 1024.0 / 1024.0, 2) AS AvailableTargetMemory_GB
FROM sys.dm_os_sys_memory;

-- 4. 已提交目标内存百分比
SELECT 
    ROUND(CONVERT(FLOAT, committed_target_kb) / NULLIF(committed_target_key, 0) * 100, 2) AS CommittedTargetPercent
FROM sys.dm_os_sys_memory;

-- 5. 内存授予
SELECT 
    granted_memory_kb / 1024 AS Granted_MB,
    waiting_for_grant_count AS WaitingForGrantCount,
    MAX(grantee_count) AS GranteeCount
FROM sys.dm_exec_query_resource_semaphores;

-- 6. 临时数据库使用情况
SELECT 
    SUM(user_objects_alloc_page_count) * 8 / 1024 AS UserObjects_MB,
    SUM(internal_objects_alloc_page_count) * 8 / 1024 AS InternalObjects_MB
FROM tempdb.sys.dm_db_session_space_usage;

8.2 IO 子系统基线

sql 复制代码
-- =============================================
-- IO 子系统基线采集
-- =============================================
SELECT 
    DB_NAMEvfs.database_id AS DatabaseName,
    mf.name AS LogicalName,
    mf.physical_name,
    vfs.num_of_reads,
    vfs.num_of_writes,
    vfs.io_stall_read_ms / NULLIF(vfs.num_of_reads, 0) AS AvgReadLatency_ms,
    vfs.io_stall_write_ms / NULLIF(vfs.num_of_writes, 0) AS AvgWriteLatency_ms,
    (vfs.io_stall_read_ms + vfs.io_stall_write_ms) / NULLIF((vfs.num_of_reads + vfs.num_of_writes), 0) AS AvgIO_Latency_ms,
    vfs.num_of_bytes_read / 1024 / 1024 AS ReadMB,
    vfs.num_of_bytes_written / 1024 / 1024 AS WriteMB,
    vf.type_desc AS FileType
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id
LEFT JOIN sys.master_files vf ON vf.database_id = vfs.database_id AND vf.file_id = vfs.file_id
ORDER BY (vfs.io_stall_read_ms + vfs.io_stall_write_ms) DESC;

-- IO 延迟阈值(SQL Server 2019 推荐值)
-- Data files:  < 20ms
-- Log files:   < 15ms
-- TempDB:      < 10ms

8.3 统计信息健康度基线

sql 复制代码
-- =============================================
-- 统计信息陈旧度基线
-- =============================================
SELECT 
    OBJECT_NAME(sp.object_id) AS TableName,
    sp.name AS StatsName,
    sp.has_filter,
    sp.filter_definition,
    STATS_DATE(sp.object_id, sp.stats_id) AS LastUpdated,
    DATEDIFF(HOUR, STATS_DATE(sp.object_id, sp.stats_id), SYSUTDGETIME()) AS Age_Hours,
    sp.user_updates,          -- 统计信息被更新的次数
    sp.user_seeks,             -- 被seek的次数
    sp.user_scans,             -- 被扫描的次数
    sp.user_lookups,           -- 被查找的次数
    s.row_count,
    s.used_page_count * 8 / 1024 AS UsedSpace_MB
FROM sys.stats sp
CROSS APPLY sys.dm_db_stats_properties(sp.object_id, sp.stats_id) sp_prop
CROSS APPLY sys.dm_db_partition_stats(s.object_id, s.partition_number, INDEX_ID_OR_STAT_ID) s
WHERE OBJECTPROPERTY(sp.object_id, 'IsUserTable') = 1
  AND sp.auto_created = 0    -- 只看手动创建的统计信息(自动创建的通常没问题)
  AND sp.is_improved = 0     -- 不看系统统计信息
ORDER BY DATEDIFF(HOUR, STATS_DATE(sp.object_id, sp.stats_id), SYSUTDGETIME()) DESC;

九、自动化基线采集框架

9.1 创建基线管理表结构

sql 复制代码
USE DBA_Monitor;
GO

-- 核心指标汇总表
CREATE TABLE PerfBaseline_Summary (
    SnapshotTime DATETIME2(0) PRIMARY KEY DEFAULT SYSUTCDATEIME(),
    InstanceName SYSNAME DEFAULT @@SERVERNAME,
    
    -- CPU 指标
    CpuUsagePercent DECIMAL(5,2),
    BatchRequestsPerSec BIGINT,
    SQLCompilationsPerSec BIGINT,
    SQLReCompilationsPerSec BIGINT,
    
    -- 内存指标
    BufferCacheHitRatio DECIMAL(5,2),
    PageLifeExpectancySec BIGINT,
    TargetServerMemKB BIGINT,
    TotalServerMemKB BIGINT,
    
    -- IO 指标
    TotalReadsPerSec BIGINT,
    TotalWritesPerSec BIGINT,
    CheckPointsPerSec BIGINT,
    LazyWritesPerSec BIGINT,
    
    -- 等待统计 Top 3
    TopWaitType_1 NVARCHAR(60),
    TopWaitType_2 NVARCHAR(60),
    TopWaitType_3 NVARCHAR(60),
    
    -- 会话统计
    ActiveSessions INT,
    RunningSessions INT,
    BlockedSessions INT
);
GO

-- 查询性能汇总表
CREATE TABLE PerfBaseline_QuerySummary (
    SnapshotTime DATETIME2(0),
    DatabaseName NVARCHAR(128),
    SQLHandle VARBINARY(64),
    PlanHandle VARBINARY(64),
    SQLText NVARCHAR(MAX),
    ExecuteCount BIGINT,
    AvgDurationMs DECIMAL(12,2),
    AvgCPUMs DECIMAL(12,2),
    AvgLogicalReads DECIMAL(12,2),
    MaxDurationMs DECIMAL(12,2),
    FirstExecution DATETIME2(0),
    LastExecution DATETIME2(0),
    PRIMARY KEY CLUSTERED (SnapshotTime, SQLHandle)
);
GO

9.2 基线采集存储过程

sql 复制代码
CREATE OR ALTER PROCEDURE CollectPerfBaselineSnapshot
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SnapshotTime DATETIME2(0) = SYSUTCDATETIME();
    
    -- ==========================================
    -- 1. 收集核心指标
    -- ==========================================
    DECLARE @CpuUsagePercent DECIMAL(5,2);
    DECLARE @BufferCacheHitRatio DECIMAL(5,2);
    DECLARE @PageLifeExpectancySec BIGINT;
    DECLARE @BatchRequestsPerSec BIGINT;
    DECLARE @TopWaitType_1 NVARCHAR(60);
    DECLARE @TopWaitType_2 NVARCHAR(60);
    DECLARE @TopWaitType_3 NVARCHAR(60);
    
    -- CPU
    SELECT TOP 1 @CpuUsagePercent = CAST(counter_value AS DECIMAL(5,2))
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Processor Information:% Processor Time'
      AND instance_name = '_Total'
      AND object_name = 'System';
    
    -- 缓冲命中率
    SELECT TOP 1 @BufferCacheHitRatio = CAST(cntr_value AS DECIMAL(5,2))
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Buffer cache hit ratio';
    
    -- PLE
    SELECT TOP 1 @PageLifeExpectancySec = cntr_value
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Page life expectancy';
    
    -- 获取 Top 等待类型
    WITH TopWaits AS (
        SELECT ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS rn, wait_type
        FROM sys.dm_os_wait_stats
        WHERE wait_type NOT IN (/* 忽略列表 */)
    )
    SELECT @TopWaitType_1 = wait_type FROM TopWaits WHERE rn = 1;
    SELECT @TopWaitType_2 = wait_type FROM TopWaits WHERE rn = 2;
    SELECT @TopWaitType_3 = wait_type FROM TopWaits WHERE rn = 3;
    
    -- 插入汇总
    INSERT INTO DBA_Monitor.dbo.PerfBaseline_Summary (
        SnapshotTime, InstanceName, CpuUsagePercent,
        BatchRequestsPerSec, SQLCompilationsPerSec, SQLReCompilationsPerSec,
        BufferCacheHitRatio, PageLifeExpectancySec,
        TopWaitType_1, TopWaitType_2, TopWaitType_3,
        ActiveSessions, RunningSessions, BlockedSessions
    )
    SELECT 
        @SnapshotTime,
        @@SERVERNAME,
        ISNULL(@CpuUsagePercent, 0),
        BR.cntr_value, SC.cntr_value, SRCN.cntr_value,
        @BufferCacheHitRatio, @PageLifeExpectancySec,
        @TopWaitType_1, @TopWaitType_2, @TopWaitType_3,
        SUM(CASE WHEN ses.status != 'sleeping' THEN 1 ELSE 0 END),
        SUM(CASE WHEN r.command IS NOT NULL THEN 1 ELSE 0 END),
        SUM(CASE WHEN r.blocking_session_id > 0 THEN 1 ELSE 0 END)
    FROM sys.dm_exec_sessions ses
    LEFT JOIN sys.dm_exec_requests r ON ses.session_id = r.session_id
    CROSS JOIN sys.dm_os_performance_counters BR
    LEFT JOIN sys.dm_os_performance_counters SC ON SC.counter_name = 'SQL Compilations/sec'
    LEFT JOIN sys.dm_os_performance_counters SRCN ON SRCN.counter_name = 'SQL Re-Compilations/sec'
    WHERE BR.counter_name = 'Batch Requests/sec';
    
    -- ==========================================
    -- 2. 收集查询性能快照
    -- ==========================================
    INSERT INTO DBA_Monitor.dbo.PerfBaseline_QuerySummary
    SELECT 
        @SnapshotTime,
        DB_NAME(qt.dbid),
        qs.sql_handle,
        qs.plan_handle,
        SUBSTRING(qt.text, (qs.statement_start_offset/2)+1, 
            ((CASE qs.statement_end_offset 
               WHEN -1 THEN DATALENGTH(qt.text)
               ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1),
        qs.execution_count,
        CAST((qs.total_elapsed_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
        CAST((qs.total_worker_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
        CAST((qs.total_logical_reads / 1.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
        qs.max_elapsed_time / 1000000.0,
        qs.first_execution_time,
        qs.last_execution_time
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
    WHERE qt.dbid > 4
      AND qt.text NOT LIKE '%sys.%'
      AND qt.text NOT LIKE '%dm_exec%'
      AND qs.execution_count > 0;
END;
GO

9.3 基线报告查询

sql 复制代码
-- =============================================
-- 基线分析报告:检测性能异常
-- =============================================

-- 1. 过去 30 天的指标趋势
SELECT 
    SnapshotTime,
    CpuUsagePercent,
    BufferCacheHitRatio,
    PageLifeExpectancySec,
    ActiveSessions,
    RunningSessions,
    BlockedSessions,
    TopWaitType_1,
    TopWaitType_2,
    TopWaitType_3
FROM DBA_Monitor.dbo.PerfBaseline_Summary
WHERE SnapshotTime >= DATEADD(DAY, -30, SYSUTCDAETIMEL())
ORDER BY SnapshotTime DESC;

-- 2. 查询性能回归检测
-- 对比最近 7 天 vs 之前 7 天的平均运行时间
DECLARE @Now DATETIME2 = SYSUTCADATEIME();

WITH BaselineData AS (
    SELECT DatabaseName, SQLText,
           AVG(AvgDurationMs) AS AvgDuration_Baseline
    FROM DBA_Monitor.dbo.PerfBaseline_QuerySummary
    WHERE SnapshotTime BETWEEN DATEADD(DAY, -14, @Now) AND DATEADD(DAY, -7, @Now)
    GROUP BY DatabaseName, SQLText
),
CurrentData AS (
    SELECT DatabaseName, SQLText,
           AVG(AvgDurationMs) AS AvgDuration_Current
    FROM DBA_Monitor.dbo.PerfBaseline_QuerySummary
    WHERE SnapshotTime >= DATEADD(DAY, -7, @Now)
    GROUP BY DatabaseName, SQLText
)
SELECT 
    cd.DatabaseName,
    cd.SQLText,
    bd.AvgDuration_Baseline,
    cd.AvgDuration_Current,
    CASE WHEN bd.AvgDuration_Baseline > 0 
         THEN CAST((cd.AvgDuration_Current - bd.AvgDuration_Baseline) / bd.AvgDuration_Baseline * 100 AS DECIMAL(5,1))
         ELSE 0 END AS Change_Pct
FROM CurrentData cd
JOIN BaselineData bd ON cd.DatabaseName = bd.DatabaseName AND cd.SQLText = bd.SQLText
WHERE bd.AvgDuration_Baseline > 0
  AND (cd.AvgDuration_Current - bd.AvgDuration_Baseline) > 100  -- 至少增加 100ms
  AND (cd.AvgDuration_Current - bd.AvgDuration_Baseline) / bd.AvgDuration_Baseline > 0.5  -- 增长超过 50%
ORDER BY Change_Pct DESC;

-- 3. 等待事件趋势
SELECT 
    SnapshotTime,
    TopWaitType_1, TopWaitType_2, TopWaitType_3,
    ActiveSessions,
    CpuUsagePercent
FROM DBA_Monitor.dbo.PerfBaseline_Summary
WHERE SnapshotTime >= DATEADD(DAY, -7, SYSUTCDATETIME())
  AND TopWaitType_1 IN (
      N'PAGEIOLATCH_EX', N'WRITELOG', N'LCK_M_X', 
      N'CXPACKET', N'RESOURCE_SEMAPHORE', N'SOS_SCHEDULER_YIELD'
  )
ORDER BY SnapshotTime DESC;

十、生产环境部署方案

10.1 SQL Agent 作业

sql 复制代码
-- 创建性能基线采集作业
USE msdb;
GO

EXEC sp_add_job
    @job_name = N'[DBA] Performance Baseline Collection',
    @enabled = 1,
    @description = N'每小时采集一次性能基线数据';
GO

EXEC sp_add_jobstep
    @job_name = N'[DBA] Performance Baseline Collection',
    @step_name = N'Collect Perf Snapshot',
    @subsystem = N'TSQL',
    @command = N'EXEC DBA_Monitor.dbo.CollectPerfBaselineSnapshot;',
    @database_name = N'master';
GO

EXEC sp_add_jobschedule
    @job_name = N'[DBA] Performance Baseline Collection',
    @name = N'Hourly',
    @freq_type = 4,      -- 每天
    @freq_interval = 1,
    @freq_subday_type = 8, -- 按小时
    @freq_subday_interval = 1;
GO

EXEC sp_add_jobserver
    @job_name = N'[DBA] Performance Baseline Collection',
    @server_name = N'(local)'
GO

10.2 监控告警规则

指标 告警阈值 级别 动作
CPU Usage > 80% 持续 10min WARNING 查 Top CPU 查询
CPU Usage > 95% 持续 5min CRITICAL 立即通知 DBA
PAGE_LATCH 等待占比 > 30% WARNING 检查 IO
WRITELOG 等待占比 > 20% WARNING 检查 Redo
CXPACKET > 40% INFO 检查并行查询
PLE < 300 sec WARNING 检查内存
PLE < 100 sec CRITICAL 可能内存不足
Blocking 链 > 5 节点 WARNING 检查事务
查询平均响应时间超基线 200% WARNING 查执行计划

十一、SQL Server 2019 CU5 特有优化特性

11.1 Intelligent Query Improvement(智能查询改进)

sql 复制代码
-- 15.0.2000.5 中的新特性
-- 在兼容性级别 150 下自动生效

-- 1. 自适应连接(Adaptive Join)
-- 根据实际数据量选择嵌套循环或哈希连接
-- 检查是否启用
SELECT name, value_in_use
FROM sys.configurations
WHERE name = 'optimize for ad hoc workloads';

-- 2. 批处理模式行存储(Batch Mode on Rowstore)
-- 允许列存储索引在行存储上也能使用批处理模式
-- 无需列存储索引即可受益

-- 3. T-SQL 增量编译(Incremental Compilation)
-- 仅重新编译受统计信息变更影响的语句,而非整个批次
-- 减少重编译开销

-- 4. 标量 UDF 内联(Scalar UDF Inlining)
-- 将标量 UDF 内联到查询中,消除额外调用开销
-- 需要开启 COMPATIBILITY_LEVEL = 150

11.2 大内存页面(Large Pages)

sql 复制代码
-- SQL Server 2019 推荐在 Linux 上也使用 Large Pages
-- Windows 下确保已启用

-- 配置 SQL Server 使用大页面
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'large pages', 1;
RECONFIGURE;
-- 重启 SQL Server 服务生效

11.3 内存优化表(In-Memory OLTP)

sql 复制代码
-- 如果需要极致性能,可以考虑 In-Memory OLTP
-- 适用于高并发、低延迟场景

-- 创建内存优化数据库
CREATE DATABASE OLTP_IM
CONTAINS MEMORY_OPTIMIZED_DATA;
GO

-- 创建内存优化表
CREATE TABLE dbo.Accounts (
    AccountID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH 
        WITH (BUCKET_COUNT = 1000000),
    Balance DECIMAL(18,4),
    ModifiedDate DATETIME2
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

十二、总结

12.1 基线采集频率建议

数据类型 采集频率 保留时间 用途
核心计数器 15 分钟 90 天 短期趋势分析
等待统计 15 分钟 180 天 瓶颈定位
查询性能(Query Store) 每小时聚合 180 天 查询回归检测
扩展事件(慢查询) 持续采集 30 天(轮转) 故障排查
执行计划统计 每小时快照 365 天 长期趋势
告警触发 实时 永久归档 快速响应

12.2 基线建设路线图

复制代码
阶段 1(第 1 周):基础数据采集
├── 启用核心性能计数器采集
├── 建立等待统计快照
└── 收集初始性能快照

阶段 2(第 2-4 周):完善指标
├── 启用 Query Store
├── 配置扩展事件会话
├── 采集 IO 和内存基线
└── 识别 Top 查询模式

阶段 3(第 5-8 周):自动化与告警
├── 建立自动化采集框架
├── 设置告警阈值
├── 生成基线报告
└── 建立变更影响评估流程

阶段 4(持续运营):持续优化
├── 定期回顾基线报告
├── 对比优化前后效果
├── 持续调整告警阈值
└── 建立容量规划模型

系列文章


总结 :性能基线不是建完就完了的事,它是要养出来的 。第一周的数据不准没关系,第二周会更准,三个月后就是一面镜子------能照出任何性能偏差。记住:没有基线就没有调优的方向,就像没有地图就没有航行的坐标!

相关推荐
MC丶科1 小时前
软考架构师90天冲刺|DAY44·Redis高级应用
数据库·数据仓库·redis·缓存·oracle·容器·规格说明书
亚林瓜子1 小时前
Google的编程Agent:antigravity CLI自动模式
ai·google·spec·coding·cli·auto·agy
LayZhangStrive1 小时前
Prompt - 如何生成贴合我们业务需求的有效prompt
ai·prompt·agent·提示词
Boop_wu1 小时前
[redis] redis 快速入门
数据库·redis·github
阿沐沐,2 小时前
Claude Code 首次连接:官方登录与第三方网关配置
人工智能·ai
SuperHeroWu72 小时前
HarmonyOS Dev Assistant (HarmonyOS开发助手)如何打通元服务开发全流程
ai·agent·harmonyos·vs code·元服务·hbuilderx·assistant
熊猫钓鱼>_>2 小时前
用 OHAudioSuite 空间渲染节点搭一座「3D 有声博物馆」——从三种模式到 FreeBuds 头追的实战记录
c++·3d·ai·harmonyos·arkts·audio·ohaudiosuite
reasonsummer2 小时前
【办公类-115-01】20260906育儿知识(家园小报)批量制作(2026年9月-2027年6月)
开发语言·数据库·c#
AC赳赳老秦2 小时前
环保监测公开数据应用:OpenClaw 抓取空气与水质公开监测数据,开展区域环境质量趋势分析
大数据·数据库·人工智能·python·php·deepseek·openclaw