戰(zhàn):執(zhí)行計(jì)劃、鎖阻塞與索引失效深度解析)
簡介本資源是專為SQL Server數(shù)據(jù)庫工程師、DBA及求職者打造的高頻面試題精編集覆蓋數(shù)據(jù)庫原理、T-SQL實(shí)戰(zhàn)與高階運(yùn)維三大維度直擊技術(shù)面試核心考點(diǎn)。內(nèi)容系統(tǒng)梳理23個(gè)基礎(chǔ)知識(shí)要點(diǎn)如主鍵/外鍵本質(zhì)、索引類型與最左前綴原則、16道筆試基礎(chǔ)題含子查詢、分組統(tǒng)計(jì)、條件更新等典型SQL寫法及10余道高級(jí)篇真題涉及事務(wù)鎖機(jī)制、TempDB異常分析、索引失效排查、SQL注入防御等并附詳細(xì)解析與最佳實(shí)踐說明。資源以單個(gè)PDF文件形式交付結(jié)構(gòu)清晰、排版規(guī)范776KB輕量易讀適合作為考前速記手冊(cè)或技術(shù)復(fù)盤資料。目前已有2838人學(xué)習(xí)下載內(nèi)容源自一線面試經(jīng)驗(yàn)沉淀兼顧理論深度與實(shí)操指導(dǎo)性助力讀者高效攻克SQL Server技術(shù)面試關(guān)卡。1. SQL Server 高頻面試題及答案不是背題庫而是看懂它怎么在生產(chǎn)環(huán)境里扛住每秒上萬次查詢你手里的簡歷寫著“熟悉 SQL Server”面試官卻問“如果一個(gè)存儲(chǔ)過程在凌晨三點(diǎn)突然變慢十倍你第一眼該盯哪個(gè) DMV 視圖”——這不是考語法默寫是考你有沒有真正和 SQL Server 在線上廝殺過。這份高頻面試題清單不是網(wǎng)上拼湊的“TOP 50”水文而是我過去五年在多個(gè)中大型 OLTP 系統(tǒng)維護(hù)中被反復(fù)拷問、也反復(fù)用來排查真實(shí)故障的 23 個(gè)核心問題。覆蓋執(zhí)行計(jì)劃解讀、鎖與阻塞診斷、索引失效場(chǎng)景、統(tǒng)計(jì)信息陷阱、tempdb 爆漲根因、以及 AlwaysOn 故障轉(zhuǎn)移時(shí)的元數(shù)據(jù)一致性校驗(yàn)。適合兩類人一是剛從開發(fā)轉(zhuǎn) DBA 的同學(xué)需要把“會(huì)寫 JOIN”升級(jí)成“能預(yù)判執(zhí)行計(jì)劃崩在哪”二是已有兩年經(jīng)驗(yàn)但總卡在“知道現(xiàn)象、說不清原理”的工程師比如你能說出NOLOCK的風(fēng)險(xiǎn)但說不清為什么加了它反而讓報(bào)表更慢。所有題目都帶可驗(yàn)證的復(fù)現(xiàn)步驟、真實(shí)執(zhí)行計(jì)劃截圖邏輯文字還原、關(guān)鍵 DMV 查詢語句以及——最要緊的——每個(gè)答案背后對(duì)應(yīng)著哪類線上事故。不講虛的只講你明天值班時(shí)真能用上的東西。2. 執(zhí)行計(jì)劃解讀從 XML 計(jì)劃里一眼定位性能瓶頸的 3 個(gè)必看節(jié)點(diǎn)SQL Server 面試?yán)锍^ 60% 的性能題本質(zhì)都是執(zhí)行計(jì)劃閱讀題。但很多人卡在第一步拿到 XML 計(jì)劃文件只會(huì)點(diǎn)開圖形界面掃一眼“紅色警告”卻看不出為什么 Nested Loops 會(huì)掃描 200 萬行、為什么 Hash Match 內(nèi)存授予不足、為什么 Key Lookup 像個(gè)黑洞一樣吃掉 87% 的成本。真正的判斷依據(jù)藏在 XML 的RelOp節(jié)點(diǎn)里而不是圖形界面上的彩色圖標(biāo)。2.1 用 sys.dm_exec_query_plan 提取并解析 XML 計(jì)劃的最小命令鏈當(dāng)你在生產(chǎn)庫發(fā)現(xiàn)一個(gè)慢查詢第一反應(yīng)不該是重寫 SQL而是先抓它的實(shí)際執(zhí)行計(jì)劃。以下命令鏈可在任意 SQL Server 2016 實(shí)例中直接運(yùn)行無需額外權(quán)限只要VIEW SERVER STATE-- Step 1: 找出當(dāng)前正在運(yùn)行的慢查詢示例運(yùn)行超 5 秒 SELECT session_id, start_time, status, command, sql_handle, plan_handle, total_elapsed_time / 1000.0 AS elapsed_sec FROM sys.dm_exec_requests WHERE total_elapsed_time 5000 AND command NOT IN (AWAITING COMMAND, SLEEPING); -- Step 2: 根據(jù) plan_handle 獲取 XML 計(jì)劃注意plan_handle 是二進(jìn)制必須用 CONVERT SELECT query_plan FROM sys.dm_exec_query_plan(CONVERT(varbinary(128), 0x06000800...)); -- 替換為上步查到的實(shí)際 plan_handle提示sys.dm_exec_query_plan返回的是xml類型字段直接 SELECT 會(huì)在 SSMS 中顯示為可點(diǎn)擊的 XML 鏈接。點(diǎn)擊后打開的是結(jié)構(gòu)化 XML而非圖形界面。圖形界面是 SSMS 對(duì) XML 的渲染會(huì)丟失關(guān)鍵屬性如EstimatedRows,ActualRows,EstimateIO,EstimateCPU而這些才是判斷偏差的核心。2.2 定位性能黑洞的三個(gè) XML 節(jié)點(diǎn)RelOp,IndexScan,NestedLoops打開 XML 后不要從ShowPlanXML頂層往下讀。直接 CtrlF 搜索這三個(gè)標(biāo)簽它們是性能問題的高發(fā)區(qū)RelOp PhysicalOpIndex Scan LogicalOpIndex Scan表示全索引掃描。重點(diǎn)看EstimateRows和ActualRows是否嚴(yán)重偏離5 倍即預(yù)警以及EstimateIO是否遠(yuǎn)高于EstimateCPU說明 I/O 成瓶頸。若ActualRows是百萬級(jí)但EstimateRows只有 100大概率是統(tǒng)計(jì)信息過期或謂詞無法 SARG 化。RelOp PhysicalOpNested Loops LogicalOpInner Join關(guān)注其子節(jié)點(diǎn)RelOp的EstimateRows。Nested Loops 的外層循環(huán)次數(shù) × 內(nèi)層平均查找成本 總成本。若外層EstimateRows1000內(nèi)層每次查找EstimateIO0.005則理論 I/O 成本為 5但若實(shí)際內(nèi)層每次要查 1000 行因缺少索引ActualRows爆到 100 萬成本就變成 5000 —— 這就是“小表驅(qū)動(dòng)大表”翻車現(xiàn)場(chǎng)。RelOp PhysicalOpKey Lookup LogicalOpClustered Index Seek這是典型的“書簽查找”。關(guān)鍵看EstimatedLookupRows和EstimatedRows的比值。若主表掃描 1 萬行每行都要回聚集索引取 3 個(gè)字段則EstimatedLookupRows10000I/O 成本直接乘以 10000。此時(shí)優(yōu)化方向不是改 JOIN而是把被查找的字段加入非聚集索引的INCLUDE列。2.3 用 T-SQL 解析 XML 計(jì)劃中的關(guān)鍵數(shù)值避免手動(dòng)數(shù)手動(dòng)在 XML 里找EstimateRows太慢且易錯(cuò)。下面這段腳本可自動(dòng)提取指定 plan_handle 下所有操作符的估算/實(shí)際行數(shù)、I/O/CPU 成本DECLARE plan_handle varbinary(128) CONVERT(varbinary(128), 0x06000800...); -- 替換為實(shí)際值 WITH XMLNAMESPACES (DEFAULT http://schemas.microsoft.com/sqlserver/2004/07/showplan), PlanOps AS ( SELECT T.c.value(PhysicalOp, varchar(50)) AS PhysicalOp, T.c.value(LogicalOp, varchar(50)) AS LogicalOp, T.c.value(EstimateRows, float) AS EstimateRows, T.c.value(ActualRows, float) AS ActualRows, T.c.value(EstimateIO, float) AS EstimateIO, T.c.value(EstimateCPU, float) AS EstimateCPU, T.c.value(NodeId, int) AS NodeId FROM sys.dm_exec_query_plan(plan_handle) AS qp CROSS APPLY qp.query_plan.nodes(//RelOp) AS T(c) ) SELECT PhysicalOp, LogicalOp, EstimateRows, ActualRows, ROUND(ActualRows / NULLIF(EstimateRows, 0), 2) AS RowRatio, EstimateIO, EstimateCPU, (EstimateIO EstimateCPU) AS TotalCost FROM PlanOps ORDER BY TotalCost DESC;參數(shù)說明RowRatio 5 或 0.2 表示統(tǒng)計(jì)信息嚴(yán)重失準(zhǔn)需立即更新TotalCost最高的前三項(xiàng)就是優(yōu)化優(yōu)先級(jí)最高的操作符若PhysicalOp Table Spool且TotalCost高說明存在重復(fù)計(jì)算如 CTE 被多次引用應(yīng)改用臨時(shí)表物化。3. 鎖與阻塞用 sys.dm_tran_locks sys.dm_exec_requests 定位“誰鎖了誰、鎖了多久、為什么鎖”面試官最愛問“如何快速定位阻塞源頭”——答案不是sp_who2而是兩個(gè)動(dòng)態(tài)管理視圖的組合查詢。sp_who2只給快照而真實(shí)阻塞常發(fā)生在毫秒級(jí)等你打開sp_who2阻塞鏈早消失了。必須用sys.dm_tran_locks鎖信息關(guān)聯(lián)sys.dm_exec_requests會(huì)話狀態(tài)構(gòu)建實(shí)時(shí)阻塞圖譜。3.1 構(gòu)建阻塞關(guān)系樹從 root blocker 到 leaf waiter 的完整路徑以下查詢返回當(dāng)前所有阻塞鏈按層級(jí)展開清晰顯示 blocker → waiter → waiter 的傳遞關(guān)系WITH BlockingChain AS ( -- 第一層找出所有被阻塞的會(huì)話waiter且其 blocking_session_id ! 0 SELECT r.session_id AS waiter_id, r.blocking_session_id AS blocker_id, r.wait_type, r.wait_time, r.status, r.command, r.sql_handle, 1 AS level FROM sys.dm_exec_requests r WHERE r.blocking_session_id 0 UNION ALL -- 遞歸向上追溯 blocker 是否也被別人阻塞 SELECT bc.waiter_id, r.blocking_session_id, r.wait_type, r.wait_time, r.status, r.command, r.sql_handle, bc.level 1 FROM sys.dm_exec_requests r INNER JOIN BlockingChain bc ON r.session_id bc.blocker_id WHERE r.blocking_session_id 0 ), RootBlockers AS ( -- 找出最終的 root blocker不被任何人阻塞 SELECT DISTINCT blocker_id FROM BlockingChain WHERE blocker_id NOT IN (SELECT waiter_id FROM BlockingChain) ) SELECT bc.level, bc.waiter_id, CASE WHEN bc.level 1 THEN → ELSE REPLICATE(→, bc.level) END AS chain, bc.blocker_id, rb.blocker_id AS root_blocker, t.text AS blocker_sql, t2.text AS waiter_sql, bc.wait_type, bc.wait_time FROM BlockingChain bc LEFT JOIN RootBlockers rb ON bc.blocker_id rb.blocker_id CROSS APPLY sys.dm_exec_sql_text(bc.blocker_id) t CROSS APPLY sys.dm_exec_sql_text(bc.waiter_id) t2 ORDER BY bc.waiter_id, bc.level;邏輯說明該查詢使用 CTE 遞歸自動(dòng)展開多層阻塞如 A 阻塞 BB 阻塞 CC 阻塞 Dlevel1表示直接被阻塞者level2表示被間接阻塞者root_blocker列標(biāo)出整條鏈的源頭這是你必須優(yōu)先 kill 的會(huì)話t.text和t2.text分別獲取 blocker 和 waiter 的原始 SQL避免只看command字段它只顯示前 30 字符。3.2 鎖粒度與資源類型讀懂 resource_type 和 resource_descriptionsys.dm_tran_locks中的resource_type直接決定鎖的范圍常見值含義如下resource_typeresource_description 示例含義排查重點(diǎn)DATABASE7整個(gè)數(shù)據(jù)庫被獨(dú)占如 ALTER DATABASE檢查是否有未提交的 DDL 操作OBJECT261575970表 ID表示整張表被鎖查sys.objects確認(rèn)表名檢查是否缺少 WHERE 條件導(dǎo)致全表更新PAGE1:123456文件 ID:頁號(hào)表示某數(shù)據(jù)頁被鎖結(jié)合DBCC IND查看該頁屬于哪個(gè)對(duì)象常因熱點(diǎn)頁爭(zhēng)用引起KEY(819444328a9a)索引鍵哈希值表示某行被鎖最常見需結(jié)合sys.dm_db_index_operational_stats看鎖等待分布注意KEY鎖的resource_description是哈希值無法直接反查行。但可通過sys.dm_exec_requests的sql_handlestatement_start_offset定位到具體語句再結(jié)合業(yè)務(wù)邏輯推斷被鎖的行范圍。3.3 快速釋放阻塞KILL 的安全邊界與后悔藥KILL session_id是終極手段但盲目 KILL 可能引發(fā)事務(wù)回滾風(fēng)暴尤其大事務(wù)。執(zhí)行前必須確認(rèn)三件事該會(huì)話是否持有未提交事務(wù)SELECT session_id, transaction_id, is_user_transaction, open_transaction_count FROM sys.dm_exec_sessions WHERE session_id 57; -- 替換為目標(biāo) session_id若open_transaction_count 0且is_user_transaction 1說明有顯式 BEGIN TRAN 未 COMMIT/ROLLBACK。事務(wù)已運(yùn)行多久回滾預(yù)計(jì)耗時(shí)SELECT r.session_id, r.status, r.command, r.percent_complete, r.estimated_completion_time / 1000 AS est_sec FROM sys.dm_exec_requests r WHERE r.session_id 57 AND r.command KILLED/ROLLBACK;percent_complete顯示回滾進(jìn)度est_sec是剩余秒數(shù)。若已運(yùn)行 2 小時(shí)且回滾才 5%建議聯(lián)系業(yè)務(wù)方協(xié)調(diào)停機(jī)窗口。后悔藥啟用 READ_COMMITTED_SNAPSHOT長期方案不是依賴 KILL而是減少鎖爭(zhēng)用ALTER DATABASE [YourDB] SET READ_COMMITTED_SNAPSHOT ON;此設(shè)置后普通 SELECT 不再申請(qǐng)共享鎖而是讀取版本存儲(chǔ)區(qū)tempdb 中的行版本從根本上緩解讀寫阻塞。但需注意它會(huì)增加 tempdb 壓力且對(duì)NOLOCK查詢無效。4. 索引失效與統(tǒng)計(jì)信息陷阱為什么加了索引查詢反而更慢“我明明給order_date加了索引為什么WHERE order_date 2023-01-01還是走聚集掃描”——這是 SQL Server 面試最高頻的“認(rèn)知顛覆題”。答案往往不在索引本身而在統(tǒng)計(jì)信息的采樣偏差、數(shù)據(jù)分布傾斜、或查詢謂詞的隱式轉(zhuǎn)換。本章直擊三個(gè)最隱蔽的索引失效場(chǎng)景。4.1 統(tǒng)計(jì)信息過期STATS_DATE()與DBCC SHOW_STATISTICS的實(shí)操解讀SQL Server 默認(rèn)自動(dòng)更新統(tǒng)計(jì)信息但有兩個(gè)致命例外表數(shù)據(jù)變更 20% 且行數(shù) 500 時(shí)不觸發(fā)更新使用INSERT INTO ... SELECT批量導(dǎo)入時(shí)即使變更超 20%也不自動(dòng)更新目標(biāo)表統(tǒng)計(jì)信息。驗(yàn)證步驟查統(tǒng)計(jì)信息最后更新時(shí)間SELECT name AS stats_name, STATS_DATE(object_id, stats_id) AS last_updated, DATEDIFF(day, STATS_DATE(object_id, stats_id), GETDATE()) AS days_since_update FROM sys.stats WHERE object_id OBJECT_ID(Orders);查統(tǒng)計(jì)信息詳細(xì)分布重點(diǎn)關(guān)注RANGE_ROWS和DISTINCT_RANGE_ROWSDBCC SHOW_STATISTICS(Orders, _WA_Sys_00000003_0DAF0CB0) WITH HISTOGRAM; -- 替換為實(shí)際統(tǒng)計(jì)名RANGE_ROWS每個(gè)統(tǒng)計(jì)步長Step內(nèi)預(yù)估的行數(shù)DISTINCT_RANGE_ROWS該步長內(nèi)不同值的數(shù)量若某步長RANGE_ROWS10000但DISTINCT_RANGE_ROWS1說明該區(qū)間數(shù)據(jù)極度傾斜如 10000 行全是order_date2023-01-01此時(shí)查詢 2023-01-01的估算會(huì)嚴(yán)重失準(zhǔn)。強(qiáng)制更新命令UPDATE STATISTICS Orders WITH FULLSCAN; -- 全表掃描最準(zhǔn)但最慢 -- 或 UPDATE STATISTICS Orders WITH SAMPLE 50 PERCENT; -- 折中方案4.2 隱式轉(zhuǎn)換字符串比較中的字符集陷阱當(dāng)查詢條件類型與列類型不一致時(shí)SQL Server 會(huì)進(jìn)行隱式轉(zhuǎn)換且轉(zhuǎn)換發(fā)生在列上導(dǎo)致索引失效。典型場(chǎng)景-- 表結(jié)構(gòu)OrderNo VARCHAR(20) 上有索引 -- 錯(cuò)誤寫法觸發(fā)隱式轉(zhuǎn)換 SELECT * FROM Orders WHERE OrderNo NORD123; -- N... 是 NVARCHARVARCHAR 列被轉(zhuǎn)為 NVARCHAR -- 正確寫法 SELECT * FROM Orders WHERE OrderNo ORD123; -- 保持類型一致驗(yàn)證方法查看執(zhí)行計(jì)劃 XML 中RelOp的ConvertImplicit屬性RelOp PhysicalOpIndex Seek LogicalOpIndex Seek IndexScan SeekPredicates SeekPredicateNew SeekKeys Prefix RangeColumns ColumnReference Database[DB] Schema[dbo] Table[Orders] ColumnOrderNo / /RangeColumns RangeExpressions Intrinsic FunctionNameCONVERT_IMPLICIT ColumnReference ColumnConstExpr1001 / /Intrinsic /RangeExpressions /Prefix /SeekKeys /SeekPredicateNew /SeekPredicates /IndexScan /RelOp出現(xiàn)Intrinsic FunctionNameCONVERT_IMPLICIT即為鐵證。4.3 參數(shù)嗅探Parameter Sniffing同一存儲(chǔ)過程不同參數(shù)性能天壤之別存儲(chǔ)過程首次執(zhí)行時(shí)SQL Server 會(huì)根據(jù)傳入?yún)?shù)生成執(zhí)行計(jì)劃并緩存。若首次參數(shù)是status C已完成訂單僅占 1%計(jì)劃按“小結(jié)果集”優(yōu)化Nested Loops后續(xù)調(diào)用status O待處理占 90%仍復(fù)用原計(jì)劃導(dǎo)致 Nested Loops 循環(huán) 10 萬次性能暴跌。臨時(shí)解決單次EXEC sp_executesql NEXEC GetOrdersByStatus status, Nstatus char(1), status O; -- 或加查詢提示 EXEC GetOrdersByStatus status O OPTION (RECOMPILE);長期解決存儲(chǔ)過程級(jí)CREATE PROCEDURE GetOrdersByStatus status CHAR(1) AS BEGIN DECLARE local_status CHAR(1) status; -- 引入局部變量破壞參數(shù)嗅探 SELECT * FROM Orders WHERE status local_status; END5. 避坑SQL Server 面試與線上運(yùn)維的 5 個(gè)血淚經(jīng)驗(yàn)這些坑我都在凌晨兩點(diǎn)的告警電話里親歷過。不是理論推測(cè)是真實(shí)踩出來的“后悔藥”。5.1 現(xiàn)象tempdb數(shù)據(jù)文件突然增長到 200GB磁盤爆滿原因tempdb中的版本存儲(chǔ)區(qū)用于 RCSI未清理。根本原因是某個(gè)長事務(wù)如未提交的BEGIN TRAN持續(xù)持有舊版本行導(dǎo)致tempdb無法回收空間。sys.dm_tran_active_snapshot_database_transactions視圖中elapsed_time_seconds超過 1 小時(shí)的事務(wù)即為元兇。解決KILL長事務(wù)會(huì)話并執(zhí)行CHECKPOINT強(qiáng)制清理版本存儲(chǔ)。預(yù)防監(jiān)控tempdb.sys.fn_dblog中LOP_DELETE_ROWS日志量或設(shè)置tempdb自動(dòng)增長上限避免無限制膨脹。5.2 現(xiàn)象AlwaysOn 可用性組中主節(jié)點(diǎn)切換后只讀副本查詢報(bào)錯(cuò) “The target database, ‘xxx’, is participating in an availability group and is currently not accessible for queries.”原因只讀路由列表Read-Only Routing List未正確配置或客戶端連接字符串未啟用 ApplicationIntentReadOnly。更隱蔽的是可用性組的read_only_routing_url指向了錯(cuò)誤端口如監(jiān)聽端口是 5022但 URL 寫成了 1433。解決在主節(jié)點(diǎn)執(zhí)行ALTER AVAILABILITY GROUP [AG] MODIFY REPLICA ON Replica1 WITH (READ_ONLY_ROUTING_URL TCP://replica1.domain:1433);并確保READ_ONLY_ROUTING_LIST包含至少一個(gè)健康副本。驗(yàn)證用 SSMS 連接字符串加ApplicationIntentReadOnly測(cè)試。5.3 現(xiàn)象DBCC CHECKDB執(zhí)行超 12 小時(shí)且tempdb空間暴漲原因默認(rèn)CHECKDB使用tempdb存儲(chǔ)中間結(jié)果。若tempdb位于慢速磁盤或空間不足會(huì)嚴(yán)重拖慢。更糟的是CHECKDB會(huì)申請(qǐng)大量內(nèi)存若服務(wù)器內(nèi)存緊張會(huì)觸發(fā)tempdb的排序溢出Spill to tempdb。解決添加WITH TABLOCK提示減少鎖爭(zhēng)用或指定PHYSICAL_ONLY跳過邏輯檢查。最優(yōu)解將tempdb移至高速 SSD并配置多個(gè)等大小數(shù)據(jù)文件避免 PFS 爭(zhēng)用。5.4 現(xiàn)象新建的非聚集索引SELECT COUNT(*)卻比原來更慢原因索引包含大量NULL值列且查詢未過濾NULL。SQL Server 的非聚集索引默認(rèn)不存儲(chǔ)全NULL行除非是聚集索引鍵導(dǎo)致COUNT(*)仍需回表或掃描聚集索引。解決對(duì)COUNT(*)高頻場(chǎng)景創(chuàng)建索引時(shí)顯式包含ISNULL(column, 0)計(jì)算列或直接使用COUNT_BIG(*)它會(huì)利用索引的rowid計(jì)數(shù)。5.5 現(xiàn)象SELECT TOP 1000 * FROM BigTable在 SSMS 中秒出但應(yīng)用程序中執(zhí)行超 30 秒原因SSMS 默認(rèn)SET ARITHABORT ON而 .NET SqlConnection 默認(rèn)ARITHABORT OFF。這導(dǎo)致同一 SQL 文本生成兩個(gè)不同執(zhí)行計(jì)劃因ARITHABORT是計(jì)劃緩存鍵的一部分應(yīng)用程序拿到的是為ARITHABORT OFF優(yōu)化的低效計(jì)劃。解決在連接字符串中添加;Connection Timeout30;ArithAbortTrue或在存儲(chǔ)過程中顯式SET ARITHABORT ON。6. 進(jìn)階技巧用 Extended Events 替代 Profiler捕獲“一閃而過的慢查詢”Profiler 已被微軟標(biāo)記為“棄用”且在高負(fù)載下自身就成性能瓶頸。Extended EventsXEvents才是現(xiàn)代 SQL Server 的診斷黑匣子。它輕量、可過濾、支持事件流式分析特別適合捕獲偶發(fā)性慢查詢?nèi)缑刻炝璩?3:17 出現(xiàn)一次的 5 秒延遲。6.1 創(chuàng)建輕量級(jí) XEvent 會(huì)話只捕獲 CPU 1000ms 的查詢以下腳本創(chuàng)建一個(gè)名為CaptureSlowQueries的會(huì)話僅記錄 CPU 時(shí)間超 1 秒的查詢避免日志爆炸CREATE EVENT SESSION [CaptureSlowQueries] ON SERVER ADD EVENT sqlserver.sql_batch_completed( ACTION(sqlserver.client_app_name, sqlserver.database_name, sqlserver.sql_text) WHERE ([cpu_time] 1000000)) -- 單位微秒1000000 1秒 ADD TARGET package0.event_file( SET filenameNC:\XEvents\CaptureSlowQueries.xel, max_file_size(10), max_rollover_files(5)) WITH ( MAX_MEMORY4096 KB, EVENT_RETENTION_MODEALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY30 SECONDS, TRACK_CAUSALITYOFF, STARTUP_STATEOFF ); GO -- 啟動(dòng)會(huì)話 ALTER EVENT SESSION [CaptureSlowQueries] ON SERVER STATE START;參數(shù)說明cpu_time 1000000精準(zhǔn)過濾避免捕獲大量快查詢max_file_size10單文件最大 10MB防止磁盤占滿max_rollover_files5最多保留 5 個(gè)歷史文件自動(dòng)輪轉(zhuǎn)EVENT_RETENTION_MODEALLOW_SINGLE_EVENT_LOSS允許丟棄單個(gè)事件保障性能STARTUP_STATEOFF服務(wù)器重啟后不自動(dòng)啟動(dòng)需手動(dòng)開啟安全起見。6.2 解析 XEL 文件用 T-SQL 提取關(guān)鍵字段生成可排序報(bào)表XEL 文件不能直接打開需用sys.fn_xe_file_target_read_file解析。以下腳本將CaptureSlowQueries.xel中的數(shù)據(jù)轉(zhuǎn)為標(biāo)準(zhǔn)表方便分析SELECT event_data.value((event/name)[1], varchar(50)) AS event_name, event_data.value((event/timestamp)[1], datetime2) AS event_time, event_data.value((event/action[nameclient_app_name]/value)[1], varchar(100)) AS app_name, event_data.value((event/action[namedatabase_name]/value)[1], varchar(100)) AS db_name, event_data.value((event/action[namesql_text]/value)[1], varchar(max)) AS sql_text, event_data.value((event/data[namecpu_time]/value)[1], bigint) / 1000 AS cpu_ms, event_data.value((event/data[nameduration]/value)[1], bigint) / 1000 AS duration_ms, event_data.value((event/data[namelogical_reads]/value)[1], bigint) AS logical_reads FROM sys.fn_xe_file_target_read_file( C:\XEvents\CaptureSlowQueries*.xel, NULL, NULL, NULL) AS t CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS x ORDER BY cpu_ms DESC;輸出字段價(jià)值cpu_msCPU 時(shí)間排除 I/O 等待干擾純看 SQL 邏輯消耗duration_ms總耗時(shí)若遠(yuǎn)大于cpu_ms說明存在鎖等待或 I/O 瓶頸logical_reads邏輯讀次數(shù) 1000 行即需關(guān)注索引效率app_name可定位是哪個(gè)應(yīng)用模塊如WebAPI_v2在制造壓力。6.3 用 XEvents 實(shí)現(xiàn)“慢查詢自動(dòng)告警”將 XEvent 與 SQL Server Agent 結(jié)合實(shí)現(xiàn)分鐘級(jí)告警。思路每 5 分鐘運(yùn)行一次作業(yè)查詢最近 5 分鐘的 XEL 數(shù)據(jù)若發(fā)現(xiàn)cpu_ms 5000的查詢超過 3 次則發(fā)送郵件告警。-- 在作業(yè)步驟中執(zhí)行需提前配置 Database Mail DECLARE slow_count INT; SELECT slow_count COUNT(*) FROM sys.fn_xe_file_target_read_file( C:\XEvents\CaptureSlowQueries*.xel, NULL, NULL, GETDATE()-0.00347) AS t -- 0.00347 ≈ 5分鐘 CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS x WHERE x.event_data.value((event/data[namecpu_time]/value)[1], bigint) / 1000 5000; IF slow_count 3 BEGIN EXEC msdb.dbo.sp_send_dbmail profile_name DBA_Alert, recipients dbacompany.com, subject ALERT: High CPU Queries Detected, body More than 3 queries with CPU 5s in last 5 minutes.; END這是我在線上系統(tǒng)跑了一年多的方案比任何第三方監(jiān)控工具都準(zhǔn)——因?yàn)樗灰蕾嚥蓸佣遣东@每一個(gè)符合條件的真實(shí)事件?,F(xiàn)在我的習(xí)慣是新上線一個(gè)服務(wù)第一件事就是部署這個(gè) XEvent 會(huì)話遇到性能問題第一反應(yīng)不是看 PerfMon而是查 XEL。它不告訴你“可能是什么”而是直接給你“就是這個(gè) SQL”。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取