SQL Server 数据库巡检报告脚本

SQL Server 数据库巡检报告脚本

SQL Server 数据库巡检报告脚本

脚本结构

复制代码
sqlserver_health_check.sql
├── 1.  实例基础信息
├── 2.  服务器资源状态
├── 3.  数据库列表与状态
├── 4.  数据文件与日志空间
├── 5.  备份状态检查
├── 6.  作业(Job)执行状况
├── 7.  索引健康度(碎片/缺失)
├── 8.  统计信息过期检查
├── 9.  TOP 高消耗 SQL
├── 10. 等待统计与阻塞
├── 11. 错误日志扫描
├── 12. AlwaysOn/镜像/日志传送状态
└── 13. 巡检告警汇总

完整脚本

sql 复制代码
/*==========================================================
  SQL Server 日常巡检报告
  适用版本: SQL Server 2014 / 2016 / 2017 / 2019 / 2022
  执行账号: sysadmin 或 VIEW SERVER STATE + VIEW ANY DATABASE
==========================================================*/

SET NOCOUNT ON;
SET ANSI_WARNINGS OFF;

PRINT '╔═══════════════════════════════════════════════════╗';
PRINT '║        SQL Server 数据库巡检报告                  ║';
PRINT '╚═══════════════════════════════════════════════════╝';
PRINT '巡检时间: ' + CONVERT(VARCHAR(20), GETDATE(), 120);
PRINT '服务器名: ' + @@SERVERNAME;
PRINT '';

/*==========================================================
  【1】实例基础信息
==========================================================*/
PRINT '┌─────────────────────────────────────┐';
PRINT '│  1. 实例基础信息                    │';
PRINT '└─────────────────────────────────────┘';

SELECT
    SERVERPROPERTY('MachineName')             AS [主机名],
    SERVERPROPERTY('ServerName')              AS [实例名],
    SERVERPROPERTY('Edition')                 AS [版本],
    SERVERPROPERTY('ProductVersion')          AS [产品版本],
    SERVERPROPERTY('ProductLevel')            AS [补丁级别],
    SERVERPROPERTY('Collation')               AS [排序规则],
    SERVERPROPERTY('IsClustered')             AS [是否群集],
    SERVERPROPERTY('IsHadrEnabled')           AS [是否启用AlwaysOn],
    SERVERPROPERTY('IsFullTextInstalled')     AS [全文索引],
    CAST(si.cpu_count AS VARCHAR)             AS [CPU核数],
    CAST(si.physical_memory_kb/1024 AS VARCHAR) + ' MB' AS [物理内存],
    si.sqlserver_start_time                   AS [启动时间],
    DATEDIFF(HOUR, si.sqlserver_start_time, GETDATE()) AS [运行小时数]
FROM sys.dm_os_sys_info si;


/*==========================================================
  【2】服务器资源状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  2. 服务器资源状态                  │';
PRINT '└─────────────────────────────────────┘';

-- 2.1 内存使用
PRINT '--- 2.1 内存使用 ---';
SELECT
    (physical_memory_in_use_kb / 1024)            AS [SQL进程使用MB],
    (locked_page_allocations_kb / 1024)           AS [锁页内存MB],
    (total_virtual_address_space_kb / 1024)       AS [虚拟地址总MB],
    (virtual_address_space_committed_kb / 1024)   AS [已提交虚拟内存MB],
    (memory_utilization_percentage)               AS [使用率%],
    process_physical_memory_low                   AS [物理内存不足],
    process_virtual_memory_low                    AS [虚拟内存不足]
FROM sys.dm_os_process_memory;

-- 2.2 Buffer Pool 命中率(关键指标,应>95%)
PRINT '--- 2.2 Buffer Pool 命中率 ---';
SELECT
    (CAST(a.cntr_value AS FLOAT) / b.cntr_value) * 100 AS [缓冲命中率%]
FROM sys.dm_os_performance_counters a
JOIN sys.dm_os_performance_counters b
    ON a.object_name = b.object_name
WHERE a.counter_name = 'Buffer cache hit ratio'
  AND b.counter_name = 'Buffer cache hit ratio base';

-- 2.3 Page Life Expectancy (PLE, 应>300秒)
PRINT '--- 2.3 页生命周期(PLE) ---';
SELECT
    object_name                              AS [对象],
    cntr_value                               AS [PLE秒],
    CASE WHEN cntr_value < 300 THEN '🔴 偏低'
         WHEN cntr_value < 1000 THEN '🟡 警告'
         ELSE '🟢 正常' END                  AS [状态]
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
  AND object_name LIKE '%Buffer Manager%';


/*==========================================================
  【3】数据库列表与状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  3. 数据库列表与状态                │';
PRINT '└─────────────────────────────────────┘';

SELECT
    database_id                              AS [DBID],
    name                                     AS [数据库名],
    state_desc                               AS [状态],
    recovery_model_desc                      AS [恢复模式],
    compatibility_level                      AS [兼容级别],
    collation_name                           AS [排序规则],
    is_read_only                             AS [只读],
    is_auto_close_on                         AS [自动关闭],
    is_auto_shrink_on                        AS [自动收缩],
    page_verify_option_desc                  AS [页校验],
    create_date                              AS [创建时间]
FROM sys.databases
ORDER BY database_id;


/*==========================================================
  【4】数据文件与日志空间
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  4. 数据文件与日志空间              │';
PRINT '└─────────────────────────────────────┘';

-- 4.1 每个数据库的文件大小和可用空间
IF OBJECT_ID('tempdb..#FileSpace') IS NOT NULL DROP TABLE #FileSpace;
CREATE TABLE #FileSpace (
    DBName         SYSNAME,
    FileType       VARCHAR(10),
    LogicalName    SYSNAME,
    PhysicalName   NVARCHAR(500),
    SizeMB         DECIMAL(18,2),
    UsedMB         DECIMAL(18,2),
    FreeMB         DECIMAL(18,2),
    UsedPct        DECIMAL(5,2),
    GrowthSetting  VARCHAR(50),
    MaxSizeMB      VARCHAR(20)
);

DECLARE @sql NVARCHAR(MAX) = N'
USE [?];
IF DB_ID(''?'') > 4 OR DB_ID(''?'') IN (1,2,4)  -- 含系统库
INSERT INTO #FileSpace
SELECT
    DB_NAME()                                          AS DBName,
    CASE f.type WHEN 0 THEN ''DATA'' WHEN 1 THEN ''LOG''
                WHEN 2 THEN ''FILESTREAM'' ELSE ''OTHER'' END AS FileType,
    f.name                                             AS LogicalName,
    f.physical_name                                    AS PhysicalName,
    CAST(f.size * 8.0 / 1024 AS DECIMAL(18,2))         AS SizeMB,
    CAST(FILEPROPERTY(f.name, ''SpaceUsed'') * 8.0 / 1024 AS DECIMAL(18,2)) AS UsedMB,
    CAST((f.size - FILEPROPERTY(f.name, ''SpaceUsed'')) * 8.0 / 1024 AS DECIMAL(18,2)) AS FreeMB,
    CAST(FILEPROPERTY(f.name, ''SpaceUsed'') * 100.0 / NULLIF(f.size,0) AS DECIMAL(5,2)) AS UsedPct,
    CASE WHEN f.is_percent_growth = 1 THEN CAST(f.growth AS VARCHAR) + ''%''
         ELSE CAST(f.growth * 8 / 1024 AS VARCHAR) + '' MB'' END AS GrowthSetting,
    CASE WHEN f.max_size = -1 THEN ''UNLIMITED''
         WHEN f.max_size = 268435456 THEN ''2TB''
         ELSE CAST(f.max_size * 8 / 1024 AS VARCHAR) + '' MB'' END AS MaxSizeMB
FROM sys.database_files f;
';

EXEC sp_MSforeachdb @sql;

SELECT
    DBName          AS [数据库],
    FileType        AS [文件类型],
    LogicalName     AS [逻辑名],
    SizeMB          AS [总大小MB],
    UsedMB          AS [已用MB],
    FreeMB          AS [剩余MB],
    UsedPct         AS [使用率%],
    CASE WHEN UsedPct >= 90 THEN '🔴 严重'
         WHEN UsedPct >= 80 THEN '🟡 警告'
         ELSE '🟢 正常' END AS [状态],
    GrowthSetting   AS [增长设置],
    MaxSizeMB       AS [最大大小],
    PhysicalName    AS [物理路径]
FROM #FileSpace
ORDER BY UsedPct DESC, DBName, FileType;

-- 4.2 磁盘可用空间
PRINT '--- 4.2 磁盘可用空间 ---';
SELECT DISTINCT
    vs.volume_mount_point                         AS [磁盘],
    vs.logical_volume_name                        AS [卷标],
    vs.file_system_type                           AS [文件系统],
    CAST(vs.total_bytes/1024.0/1024/1024 AS DECIMAL(10,2))     AS [总GB],
    CAST(vs.available_bytes/1024.0/1024/1024 AS DECIMAL(10,2)) AS [可用GB],
    CAST((1 - vs.available_bytes*1.0/vs.total_bytes)*100 AS DECIMAL(5,2)) AS [使用率%],
    CASE WHEN (1 - vs.available_bytes*1.0/vs.total_bytes) >= 0.9 THEN '🔴 严重'
         WHEN (1 - vs.available_bytes*1.0/vs.total_bytes) >= 0.8 THEN '🟡 警告'
         ELSE '🟢 正常' END AS [状态]
FROM sys.master_files mf
CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs;


/*==========================================================
  【5】备份状态检查
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  5. 备份状态检查                    │';
PRINT '└─────────────────────────────────────┘';

SELECT
    d.name                                     AS [数据库],
    d.recovery_model_desc                      AS [恢复模式],
    MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END) AS [最近完整备份],
    MAX(CASE WHEN b.type='I' THEN b.backup_finish_date END) AS [最近差异备份],
    MAX(CASE WHEN b.type='L' THEN b.backup_finish_date END) AS [最近日志备份],
    DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) AS [完整备份距今小时],
    DATEDIFF(HOUR, MAX(CASE WHEN b.type='L' THEN b.backup_finish_date END), GETDATE()) AS [日志备份距今小时],
    CASE
        WHEN MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END) IS NULL THEN '🔴 从未备份'
        WHEN DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) > 24*7 THEN '🔴 超过一周'
        WHEN DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) > 24 THEN '🟡 超过一天'
        ELSE '🟢 正常' END                     AS [备份状态]
FROM sys.databases d
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name
WHERE d.database_id > 4       -- 排除系统库(按需注释)
  AND d.state_desc = 'ONLINE'
GROUP BY d.name, d.recovery_model_desc
ORDER BY [完整备份距今小时] DESC;


/*==========================================================
  【6】作业(Job)执行状况
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  6. SQL Agent 作业执行状况          │';
PRINT '└─────────────────────────────────────┘';

SELECT
    j.name                                   AS [作业名],
    CASE j.enabled WHEN 1 THEN '启用' ELSE '禁用' END AS [状态],
    CASE h.run_status
         WHEN 0 THEN '❌ 失败' WHEN 1 THEN '✅ 成功'
         WHEN 2 THEN '重试'   WHEN 3 THEN '取消'
         WHEN 4 THEN '运行中' ELSE '未知' END  AS [上次结果],
    msdb.dbo.agent_datetime(h.run_date, h.run_time) AS [上次运行时间],
    STUFF(STUFF(RIGHT('000000' + CAST(h.run_duration AS VARCHAR), 6), 5, 0, ':'), 3, 0, ':') AS [持续时长],
    SUBSTRING(h.message, 1, 100)             AS [消息摘要]
FROM msdb.dbo.sysjobs j
OUTER APPLY (
    SELECT TOP 1 run_status, run_date, run_time, run_duration, message
    FROM msdb.dbo.sysjobhistory
    WHERE job_id = j.job_id AND step_id = 0
    ORDER BY run_date DESC, run_time DESC
) h
ORDER BY
    CASE WHEN h.run_status = 0 THEN 0 ELSE 1 END,  -- 失败的排前面
    h.run_date DESC;


/*==========================================================
  【7】索引健康度(碎片 & 缺失 & 未用)
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  7. 索引健康度检查                  │';
PRINT '└─────────────────────────────────────┘';

-- 7.1 碎片率 > 30% 的索引(需重建)
PRINT '--- 7.1 高碎片索引 (>30%) ---';
SELECT TOP 30
    DB_NAME(ips.database_id)                 AS [数据库],
    OBJECT_SCHEMA_NAME(ips.object_id, ips.database_id) + '.' +
    OBJECT_NAME(ips.object_id, ips.database_id) AS [表],
    i.name                                   AS [索引名],
    ips.index_type_desc                      AS [索引类型],
    CAST(ips.avg_fragmentation_in_percent AS DECIMAL(5,2)) AS [碎片率%],
    ips.page_count                           AS [页数],
    CASE
        WHEN ips.avg_fragmentation_in_percent > 30 THEN 'REBUILD'
        WHEN ips.avg_fragmentation_in_percent > 10 THEN 'REORGANIZE'
        ELSE 'OK' END                        AS [建议操作]
FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
    ON ips.object_id = i.object_id
   AND ips.index_id  = i.index_id
WHERE ips.avg_fragmentation_in_percent > 30
  AND ips.page_count > 1000             -- 小表忽略
  AND ips.database_id > 4
  AND i.name IS NOT NULL
ORDER BY ips.avg_fragmentation_in_percent DESC;

-- 7.2 缺失索引建议(按收益排序)
PRINT '--- 7.2 系统推荐的缺失索引 (TOP 10) ---';
SELECT TOP 10
    DB_NAME(mid.database_id)                 AS [数据库],
    OBJECT_NAME(mid.object_id, mid.database_id) AS [表名],
    CAST(migs.avg_total_user_cost * migs.avg_user_impact *
         (migs.user_seeks + migs.user_scans) AS DECIMAL(18,2)) AS [预估收益],
    migs.user_seeks                          AS [Seek次数],
    migs.user_scans                          AS [Scan次数],
    migs.last_user_seek                      AS [最近使用],
    mid.equality_columns                     AS [等值列],
    mid.inequality_columns                   AS [不等值列],
    mid.included_columns                     AS [包含列],
    'CREATE INDEX IX_' + OBJECT_NAME(mid.object_id, mid.database_id) +
        '_' + REPLACE(REPLACE(REPLACE(ISNULL(mid.equality_columns,''), '[',''), ']',''), ', ','_') +
        ' ON ' + mid.statement +
        ' (' + ISNULL(mid.equality_columns,'') +
        CASE WHEN mid.inequality_columns IS NOT NULL
             THEN ISNULL(',' + mid.inequality_columns, '') ELSE '' END + ')' +
        ISNULL(' INCLUDE (' + mid.included_columns + ')', '') AS [建议创建语句]
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig
    ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs
    ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id > 4
ORDER BY [预估收益] DESC;


/*==========================================================
  【8】统计信息过期检查
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  8. 统计信息过期检查                │';
PRINT '└─────────────────────────────────────┘';

IF OBJECT_ID('tempdb..#StatsInfo') IS NOT NULL DROP TABLE #StatsInfo;
CREATE TABLE #StatsInfo (
    DBName SYSNAME, TableName SYSNAME, StatName SYSNAME,
    LastUpdated DATETIME, RowsInTable BIGINT, RowsSampled BIGINT,
    ModifiedRows BIGINT, ModifiedPct DECIMAL(5,2)
);

EXEC sp_MSforeachdb N'
USE [?];
IF DB_ID() > 4
INSERT INTO #StatsInfo
SELECT
    DB_NAME(),
    OBJECT_SCHEMA_NAME(s.object_id) + ''.'' + OBJECT_NAME(s.object_id),
    s.name,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter,
    CAST(sp.modification_counter * 100.0 / NULLIF(sp.rows,0) AS DECIMAL(5,2))
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECTPROPERTY(s.object_id, ''IsUserTable'') = 1
  AND sp.rows > 10000
  AND (sp.last_updated < DATEADD(DAY, -7, GETDATE())
       OR sp.modification_counter > sp.rows * 0.2);
';

SELECT TOP 30
    DBName         AS [数据库],
    TableName      AS [表],
    StatName       AS [统计信息],
    LastUpdated    AS [最后更新],
    DATEDIFF(DAY, LastUpdated, GETDATE()) AS [距今天数],
    RowsInTable    AS [表行数],
    ModifiedRows   AS [修改行数],
    ModifiedPct    AS [修改比例%]
FROM #StatsInfo
ORDER BY ModifiedPct DESC;


/*==========================================================
  【9】TOP 高消耗 SQL
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  9. TOP 高消耗 SQL                  │';
PRINT '└─────────────────────────────────────┘';

-- 9.1 CPU 消耗 TOP 10
PRINT '--- 9.1 CPU 消耗 TOP 10 ---';
SELECT TOP 10
    qs.execution_count                      AS [执行次数],
    qs.total_worker_time / 1000             AS [总CPU毫秒],
    qs.total_worker_time / qs.execution_count / 1000 AS [平均CPU毫秒],
    qs.total_elapsed_time / qs.execution_count / 1000 AS [平均耗时毫秒],
    qs.total_logical_reads / qs.execution_count AS [平均逻辑读],
    qs.last_execution_time                  AS [最后执行时间],
    DB_NAME(qt.dbid)                        AS [数据库],
    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) AS [SQL语句]
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.execution_count > 0
ORDER BY qs.total_worker_time DESC;

-- 9.2 逻辑读消耗 TOP 10
PRINT '--- 9.2 逻辑读 TOP 10 ---';
SELECT TOP 10
    qs.execution_count                                    AS [执行次数],
    qs.total_logical_reads                                AS [总逻辑读],
    qs.total_logical_reads / qs.execution_count           AS [平均逻辑读],
    qs.total_physical_reads / qs.execution_count          AS [平均物理读],
    qs.last_execution_time                                AS [最后执行],
    DB_NAME(qt.dbid)                                      AS [数据库],
    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) AS [SQL语句]
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.execution_count > 0
ORDER BY qs.total_logical_reads DESC;


/*==========================================================
  【10】等待统计 & 当前阻塞
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  10. 等待统计 & 当前阻塞            │';
PRINT '└─────────────────────────────────────┘';

-- 10.1 TOP 等待事件
PRINT '--- 10.1 TOP 15 等待事件 ---';
SELECT TOP 15
    wait_type                                AS [等待类型],
    waiting_tasks_count                      AS [等待次数],
    wait_time_ms                             AS [总等待ms],
    wait_time_ms / NULLIF(waiting_tasks_count,0) AS [平均等待ms],
    signal_wait_time_ms                      AS [信号等待ms],
    CAST(signal_wait_time_ms * 100.0 / NULLIF(wait_time_ms,0) AS DECIMAL(5,2)) AS [CPU压力%]
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
    'SLEEP_TASK','BROKER_TASK_STOP','SQLTRACE_BUFFER_FLUSH',
    'CLR_AUTO_EVENT','CLR_MANUAL_EVENT','LAZYWRITER_SLEEP',
    'CHECKPOINT_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH',
    'XE_TIMER_EVENT','XE_DISPATCHER_WAIT','LOGMGR_QUEUE',
    'FT_IFTS_SCHEDULER_IDLE_WAIT','BROKER_TO_FLUSH',
    'BROKER_EVENTHANDLER','TRACEWRITE','FT_IFTSHC_MUTEX',
    'SQLTRACE_INCREMENTAL_FLUSH_SLEEP','DIRTY_PAGE_POLL',
    'SP_SERVER_DIAGNOSTICS_SLEEP','HADR_FILESTREAM_IOMGR_IOCOMPLETION',
    'HADR_WORK_QUEUE','QDS_ASYNC_QUEUE','QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
    'WAITFOR','DISPATCHER_QUEUE_SEMAPHORE','BROKER_RECEIVE_WAITFOR'
)
  AND waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;

-- 10.2 当前阻塞会话链
PRINT '--- 10.2 当前阻塞链 ---';
SELECT
    s.session_id                             AS [会话SID],
    s.blocking_session_id                    AS [阻塞者SID],
    s.status                                 AS [状态],
    s.wait_type                              AS [等待类型],
    s.wait_time                              AS [等待ms],
    s.wait_resource                          AS [等待资源],
    DB_NAME(s.database_id)                   AS [数据库],
    es.login_name                            AS [登录],
    es.host_name                             AS [主机],
    es.program_name                          AS [应用],
    SUBSTRING(qt.text, s.statement_start_offset/2+1,
        (CASE s.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
              ELSE s.statement_end_offset END - s.statement_start_offset)/2 + 1) AS [正在执行的SQL]
FROM sys.dm_exec_requests s
JOIN sys.dm_exec_sessions es ON s.session_id = es.session_id
OUTER APPLY sys.dm_exec_sql_text(s.sql_handle) qt
WHERE s.blocking_session_id <> 0
   OR s.session_id IN (SELECT blocking_session_id
                       FROM sys.dm_exec_requests
                       WHERE blocking_session_id <> 0);


/*==========================================================
  【11】错误日志扫描(最近24小时)
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  11. 错误日志关键字扫描             │';
PRINT '└─────────────────────────────────────┘';

IF OBJECT_ID('tempdb..#ErrLog') IS NOT NULL DROP TABLE #ErrLog;
CREATE TABLE #ErrLog (
    LogDate DATETIME,
    ProcessInfo VARCHAR(50),
    LogText NVARCHAR(MAX)
);

INSERT INTO #ErrLog
EXEC xp_readerrorlog 0, 1, NULL, NULL,
     @p5 = NULL, @p6 = NULL, @p7 = N'desc';

SELECT TOP 50
    LogDate                                 AS [时间],
    ProcessInfo                             AS [进程],
    LEFT(LogText, 200)                      AS [日志摘要]
FROM #ErrLog
WHERE LogDate >= DATEADD(HOUR, -24, GETDATE())
  AND (
       LogText LIKE '%error%'
    OR LogText LIKE '%fail%'
    OR LogText LIKE '%severity: 1[6-9]%'
    OR LogText LIKE '%severity: 2%'
    OR LogText LIKE '%corrupt%'
    OR LogText LIKE '%deadlock%'
    OR LogText LIKE '%I/O%error%'
    OR LogText LIKE '%timeout%'
  )
ORDER BY LogDate DESC;


/*==========================================================
  【12】AlwaysOn / 镜像 / 日志传送 状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  12. 高可用组件状态                 │';
PRINT '└─────────────────────────────────────┘';

-- 12.1 AlwaysOn
IF SERVERPROPERTY('IsHadrEnabled') = 1
BEGIN
    PRINT '--- 12.1 AlwaysOn 可用性组 ---';

    SELECT
        ag.name                              AS [可用性组],
        ar.replica_server_name               AS [副本服务器],
        ars.role_desc                        AS [角色],
        ars.connected_state_desc             AS [连接状态],
        ars.synchronization_health_desc      AS [同步健康度],
        ars.operational_state_desc           AS [运行状态],
        ar.availability_mode_desc            AS [同步模式],
        ar.failover_mode_desc                AS [故障转移模式]
    FROM sys.availability_groups ag
    JOIN sys.availability_replicas ar ON ag.group_id = ar.group_id
    JOIN sys.dm_hadr_availability_replica_states ars
         ON ar.replica_id = ars.replica_id;

    PRINT '--- 12.2 AlwaysOn 数据库同步延迟 ---';
    SELECT
        DB_NAME(drs.database_id)             AS [数据库],
        ar.replica_server_name               AS [副本],
        drs.synchronization_state_desc       AS [同步状态],
        drs.synchronization_health_desc      AS [健康度],
        drs.log_send_queue_size              AS [日志发送队列KB],
        drs.redo_queue_size                  AS [重做队列KB],
        drs.redo_rate                        AS [重做速率KB/s],
        drs.last_commit_time                 AS [最后提交时间]
    FROM sys.dm_hadr_database_replica_states drs
    JOIN sys.availability_replicas ar ON drs.replica_id = ar.replica_id;
END
ELSE
    PRINT '    (本实例未启用 AlwaysOn)';

-- 12.2 数据库镜像
IF EXISTS (SELECT 1 FROM sys.database_mirroring WHERE mirroring_state IS NOT NULL)
BEGIN
    PRINT '--- 12.3 数据库镜像 ---';
    SELECT
        DB_NAME(database_id)                 AS [数据库],
        mirroring_role_desc                  AS [角色],
        mirroring_state_desc                 AS [状态],
        mirroring_safety_level_desc          AS [安全级别],
        mirroring_partner_name               AS [伙伴],
        mirroring_witness_name               AS [见证]
    FROM sys.database_mirroring
    WHERE mirroring_state IS NOT NULL;
END


/*==========================================================
  【13】巡检告警汇总
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│  13. 巡检告警汇总                   │';
PRINT '└─────────────────────────────────────┘';

IF OBJECT_ID('tempdb..#Alert') IS NOT NULL DROP TABLE #Alert;
CREATE TABLE #Alert (AlertLevel VARCHAR(10), AlertItem NVARCHAR(500));

-- 数据库非ONLINE状态
INSERT #Alert
SELECT '🔴 严重', '数据库 [' + name + '] 状态异常: ' + state_desc
FROM sys.databases WHERE state_desc <> 'ONLINE';

-- 文件使用率 > 85%
INSERT #Alert
SELECT
    CASE WHEN UsedPct >= 95 THEN '🔴 严重' ELSE '🟡 警告' END,
    '数据库[' + DBName + ']文件[' + LogicalName + ']使用率: ' +
    CAST(UsedPct AS VARCHAR) + '%'
FROM #FileSpace WHERE UsedPct >= 85;

-- 备份告警
INSERT #Alert
SELECT '🔴 严重', '数据库 [' + d.name + '] 从未完整备份'
FROM sys.databases d
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
WHERE d.database_id > 4 AND d.state_desc = 'ONLINE'
GROUP BY d.name
HAVING MAX(b.backup_finish_date) IS NULL;

INSERT #Alert
SELECT '🟡 警告',
    '数据库 [' + d.name + '] 完整备份已超过24小时: 最后备份 ' +
    CONVERT(VARCHAR(20), MAX(b.backup_finish_date), 120)
FROM sys.databases d
JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
WHERE d.database_id > 4 AND d.state_desc = 'ONLINE'
GROUP BY d.name
HAVING DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE()) > 24;

-- 失败的Job
INSERT #Alert
SELECT '🔴 严重', '作业 [' + j.name + '] 最近执行失败'
FROM msdb.dbo.sysjobs j
WHERE j.enabled = 1
  AND EXISTS (
      SELECT 1 FROM msdb.dbo.sysjobhistory h
      WHERE h.job_id = j.job_id AND h.step_id = 0
        AND h.run_status = 0
        AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(DAY, -1, GETDATE())
  );

-- PLE 过低
INSERT #Alert
SELECT '🟡 警告', '页生命周期(PLE)过低: ' + CAST(cntr_value AS VARCHAR) + ' 秒'
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
  AND object_name LIKE '%Buffer Manager%'
  AND cntr_value < 300;

-- 存在阻塞
INSERT #Alert
SELECT '🟡 警告',
    '当前存在阻塞会话: ' + CAST(COUNT(*) AS VARCHAR) + ' 个会话被阻塞'
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
HAVING COUNT(*) > 0;

-- 自动收缩/自动关闭
INSERT #Alert
SELECT '🟡 警告', '数据库 [' + name + '] 启用了不推荐的选项: ' +
    CASE WHEN is_auto_shrink_on = 1 THEN 'AUTO_SHRINK ' ELSE '' END +
    CASE WHEN is_auto_close_on = 1  THEN 'AUTO_CLOSE'  ELSE '' END
FROM sys.databases
WHERE database_id > 4
  AND (is_auto_shrink_on = 1 OR is_auto_close_on = 1);

-- 输出告警
IF EXISTS (SELECT 1 FROM #Alert)
    SELECT AlertLevel AS [级别], AlertItem AS [告警项]
    FROM #Alert
    ORDER BY CASE AlertLevel WHEN '🔴 严重' THEN 1 WHEN '🟡 警告' THEN 2 ELSE 3 END;
ELSE
    PRINT '✅ 未发现异常项,数据库运行正常';

PRINT '';
PRINT '═══════════ 巡检完成 ═══════════';

-- 清理临时表
DROP TABLE IF EXISTS #FileSpace;
DROP TABLE IF EXISTS #StatsInfo;
DROP TABLE IF EXISTS #ErrLog;
DROP TABLE IF EXISTS #Alert;

使用方式

方式一:SSMS 手动执行

  1. 打开 SQL Server Management Studio
  2. 连接实例(用 sysadmin 账号)
  3. 新建查询,粘贴上述脚本
  4. 结果输出到**"结果到文本"**模式(Ctrl+T)阅读更清晰

方式二:命令行导出报告

cmd 复制代码
sqlcmd -S YourServer -E -i health_check.sql -o C:\DBA\Report\Check_20260422.txt -w 500

方式三:定时任务自动化(推荐)

建一个 SQL Agent 作业,每日早上 8 点执行并邮件推送:

sql 复制代码
-- 在作业步骤中使用 sqlcmd + sp_send_dbmail
DECLARE @ReportFile NVARCHAR(200) =
    'C:\DBA\Report\Check_' + FORMAT(GETDATE(),'yyyyMMdd') + '.txt';

EXEC xp_cmdshell 'sqlcmd -S . -E -i C:\DBA\Scripts\health_check.sql -o ... -w 500';

EXEC msdb.dbo.sp_send_dbmail
    @profile_name = 'DBA_Mail',
    @recipients   = 'dba@company.com',
    @subject      = 'SQL Server 日常巡检报告',
    @body         = '附件为今日巡检结果,请查收。',
    @file_attachments = @ReportFile;

关键指标阈值参考

监控项 正常 警告 严重
Buffer 命中率 > 99% 95%~99% < 95%
PLE 页生命周期 > 1000秒 300~1000秒 < 300秒
数据文件使用率 < 80% 80%~90% > 90%
磁盘可用空间 > 20% 10%~20% < 10%
索引碎片率 < 10% 10%~30% > 30%
备份间隔 < 24小时 24~72小时 > 72小时
AlwaysOn 重做队列 < 1MB 1MB~100MB > 100MB

建议的巡检频率

巡检频率 检查内容
每日 1/2/3/5/6/10/11/13 节
每周 7/8 节(索引碎片、统计信息)
每月 全量完整报告并归档留存

脚本包含的 13 项检查可按需裁剪,建议第一次运行先在测试库验证,再部署到生产环境。

相关推荐
難釋懷2 小时前
Redis服务器端优化-命令及安全配置
数据库·redis·安全
maqr_1102 小时前
PHP怎么记录SQL日志_PDOStatement拦截查询语句【详解】
jvm·数据库·python
jeCA EURG2 小时前
完美解决phpstudy安装后mysql无法启动
数据库·mysql
2401_882273722 小时前
如何通过MongoDB GridFS实现文件的分块下载
jvm·数据库·python
weixin_580614002 小时前
CSS如何实现动态背景色线性渐变_利用CSS变量控制渐变方向
jvm·数据库·python
施棠海2 小时前
SQLite姓氏数据库首字母检索开发
数据库·oracle
weixin_408717772 小时前
mysql如何查询所有列_mysql select星号性能分析
jvm·数据库·python
a9511416422 小时前
mysql权限表查询性能如何优化_MySQL系统权限缓存原理
jvm·数据库·python
zxrhhm2 小时前
Oracle RAC 日常监控脚本
数据库·oracle