一次性能優化實戰經歷

每次經歷數據庫性能調優,都是對性能優化的再次認識、對本身知識不足的有力驗證,只有不斷總結、學習才能少走彎路。數據庫


1、性能問題描述


應用端反應系統查詢緩慢,長時間出不來結果。SQLServer數據庫服務器吞吐量不足,CPU資源不足,常常飆到100%…….緩存


2、監測分析


收集性能數據採用二種方式:連續一段時間收集和高峯期實時收集性能優化


連續一天收集性能指標(如下簡稱「連續監測」)服務器


目的: 經過此方式獲得CPU/內存/磁盤/SQLServer整體狀況,宏觀上分析當前服務器的主要的性能瓶頸。session


工具: 性能計數器 Perfmon+PAL日誌分析器架構


配置:併發


  1. Perfmon配置主要性能計數器內容具體以下表app


  2. Perfmon收集的時間間隔:15秒 (不宜太短,不然會對服務器性能形成額外壓力)ide


  3. 收集時間:  8:00~20:00業務時間,收集一天函數



分析監測結果


收集完成後,經過PAL工具自動分析出結果,顯示主要性能問題:


業務高峯期CPU接近100%,並伴隨較多的Latch(閂鎖)等待,查詢時有大量的掃表操做。這些只是宏觀上獲得的「現象級「的性能問題表現,並不能必定說明是CPU資源不夠致使的,須要進一步找證據分析。


PAL分析得出幾個突出性能問題


1. 業務高峯期CPU接近瓶頸:CPU平均在60%左右,高峯在80%以上,極端達到100%



2. Latch等待一直持續存在,平均在>500。Non-Page Latch等待嚴重




3. 業務高峯期有大量的表掃描



4. SQL編譯和反編譯參數高於正常



5.PLE即頁在內存中的生命週期,其數量從某個時間點出現斷崖式降低


其數量從早上某個時間點降低後直持續到下午4點,說明這段時間內存中頁面切換比較頻繁,出現從磁盤讀取大量頁數據到內存,極可能是大面積掃表致使。



實時監測性能指標


目的: 根據「連續監測「已知的業務高峯期PeakTime主要發生時段,接下來經過實時監測重點關注這段時間各項指標,進一步確認問題。


工具: SQLCheck(工具使用介紹文章後面會發出)


配置: 客戶端鏈接到SQLCheck配置


小貼士:建議不要在當前服務器運行,可選擇另一臺機器運行SQLCheck


分析監測結果


實時監測顯示Non-Page Latch等待嚴重,這點與上面「連續監測」獲得結果一直

Session之間阻塞現象時常發生,經分析是大的結果集查詢阻塞了別的查詢、更新、刪除操做致使


詳細分析


數據庫存存在大量表掃描操做,致使緩存中數據不能知足查詢,須要從磁盤中讀取數據,產生IO等待致使阻塞。


 1. Non-Page Latch等待時間長



2. 當 Non-Page Latch等待發生時候,實時監測顯示正在執行大的查詢操做



3. 伴有session之間阻塞現象,在大的查詢時發生阻塞現象,CPU也隨之飆到95%以上



解決方案


找到問題語句,建立基於條件的索引來減小掃描,並更新統計信息。


上面方法還沒法解決,考慮將受影響的數據轉移到更快的IO子系統,考慮增長內存。


3、等待類型分析


經過等待類型,換個角度進一步分析到底時哪些資源出現瓶頸


工具:  DMV/DMO


操做:


1. 先清除歷史等待數據


選擇早上8點左右執行下面語句


DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR);


2. 晚上8點左右執行,執行下面語句收集Top 10的等待類型信息統計。





3.提取信息



查詢結果得出排名:


1:CXPACKET

2:LATCH_X

3:IO_COMPITION

4:SOS_SCHEDULER_YIELD

5:   ASYNC_NETWORK_IO

6.   PAGELATCH_XX

7/8.PAGEIOLATCH_XX


跟主要資源相關的等待方陣以下:


CPU相關:CXPACKET 和SOS_SCHEDULER_YIELD

IO相關: PAGEIOLATCH_XXIO_COMPLETION

Memory相關: PAGELATCH_XX、LATCH_X


進一步分析前幾名等待類型


當前排前三位:CXPACKET、LATCH_EX、IO_COMPLETION等待,開始一個個分析其產生等待背後緣由


CXPACKET等待分析


CXPACKET等待排第1位, SOS_SCHEDULER_YIELD排在4位,伴有第七、8位的PAGEIOLATCH_XX等待。發生了並行操做worker被阻塞


說明:


1.    存在大範圍的表Scan


2.    某些並行線程執行時間過長,這個要將PAGEIOLATCH_XX和非頁閂鎖Latch_XX的ACCESS_METHODS_DATASET_PARENT Latch結合起來看,後面會給到相關信息


3.    執行計劃不合理的可能


分析:


1.     首先看一下花在執行等待和資源等待的時間


2.     PAGEIOLATCH_XX是否存在,PAGEIOLATCH_SH等待,這意味着大範圍SCAN


3.     是否同時有ACCESS_METHODS_DATASET_PARENT Latch或ACCESS_METHODS_SCAN_RANGE_GENERATOR LATCH等待


4.     執行計劃是否合理


信提取息:


獲取CPU的執行等待和資源等待的時間所佔比重


執行下面語句:


--CPU Wait Queue (threshold<=6)

select  scheduler_id,idle_switches_count,context_switches_count,current_tasks_count, active_workers_count from  sys.dm_os_schedulers

where scheduler_id<255


SELECT  sum(signal_wait_time_ms) as total_signal_wait_time_ms,

sum(wait_time_ms-signal_wait_time_ms) as resource_wait_time_percent,

sum(signal_wait_time_ms)*1.0/sum(wait_time_ms)*100 as signal_wait_percent,

sum(wait_time_ms-signal_wait_time_ms)*1.0/sum(wait_time_ms)*100 asresource_wait_percent  FROM  SYS.dm_os_wait_stats



結論:從下表收集到信息CPU主要花在資源等待上,而執行時候等待佔比率小,因此不能武斷認爲CPU資源不夠。


形成緣由:


缺乏彙集索引、不許確的執行計劃、並行線程執行時間過長、是否存在隱式轉換、TempDB資源爭用


解決方案:


主要從如何減小CPU花在資源等待的時間


1.    設置查詢的MAXDOP,根據CPU核數設置合適的值(解決多CPU並行處理出現水桶短板現象)


2.    檢查」cost threshold parallelism」的值,設置爲更合理的值


3.    減小全表掃描:創建合適的彙集索引、非彙集索引,減小全表掃描


4.    不精確的執行計劃:選用更優化執行計劃


5.    統計信息:確保統計信息是最新的


6.    建議添加多個Temp DB 數據文件,減小Latch爭用,最佳實踐:>8核數,建議添加4個或8個等大小的數據文件


LATCH_EX等待分析


LATCH_EX等待排第2位。


說明:


有大量的非頁閂鎖等待,首先確認是哪個閂鎖等待時間過長,是否同時發生CXPACKET等待類型。


分析:


查詢全部閂鎖等待信息,發現ACCESS_METHODS_DATASET_PARENT等待最長,查詢相關資料顯示因從磁盤->IO讀取大量的數據到緩存,結合與以前Perfmon結果作綜合分析判斷,判斷存在大量掃描。


運行腳本


SELECT * FROM sys.dm_os_latch_stats


信提取息:



形成緣由:


有大量的並行處理等待、IO頁面處理等待,這進一步推定存在大範圍的掃描表操做。


與開發人員確認存儲過程當中使用大量的臨時表,並監測到業務中處理用頻繁使用臨時表、標量值函數,不斷建立用戶對象等,TEMPDB 處理內存相關PFSGAMSGAM時,有不少內部資源申請徵用的Latch等待現象。


解決方案:


1.    優化TempDB

2.    建立非彙集索引來減小掃描

3.    更新統計信息

4.    在上面方法仍然沒法解決,可將受影響的數據轉移到更快的IO子系統,考慮增長內存


IO_COMPLETION等待分析


現象:


IO_COMPLETION等待排第3位


說明:


IO延遲問題,數據從磁盤到內存等待時間長


分析:


從數據庫的文件讀寫效率分析哪一個比較慢,再與「CXPACKET等待分析」的結果合起來分析。


Temp IO讀/寫資源效率


1.    TempDB的數據文件的平均IO在80左右,這個超出通常值,TempDB存在嚴重的延遲。


2.    TempDB所在磁盤的Read latency爲65,也比通常值偏高。


運行腳本:


--數據庫文件讀寫IO性能

SELECT DB_NAME(fs.database_id) AS [Database Name], CAST(fs.io_stall_read_ms/(1.0 + fs.num_of_reads) ASNUMERIC(10,1)) AS [avg_read_stall_ms],

CAST(fs.io_stall_write_ms/(1.0 + fs.num_of_writes) AS NUMERIC(10,1)) AS [avg_write_stall_ms],

CAST((fs.io_stall_read_ms + fs.io_stall_write_ms)/(1.0 + fs.num_of_reads + fs.num_of_writes) AS NUMERIC(10,1)) AS[avg_io_stall_ms],

CONVERT(DECIMAL(18,2), mf.size/128.0) AS [File Size (MB)], mf.physical_name, mf.type_desc, fs.io_stall_read_ms,fs.num_of_reads,

fs.io_stall_write_ms, fs.num_of_writes, fs.io_stall_read_ms + fs.io_stall_write_ms AS [io_stalls], fs.num_of_reads +fs.num_of_writes AS [total_io]

FROM sys.dm_io_virtual_file_stats(null,null) AS fs

INNER JOIN sys.master_files AS mf WITH (NOLOCK)

ON fs.database_id = mf.database_id

AND fs.[file_id] = mf.[file_id]

ORDER BY avg_io_stall_ms DESC OPTION (RECOMPILE);

 

--驅動磁盤-IO文件狀況

SELECT [Drive],

       CASE

              WHEN num_of_reads = 0 THEN 0

              ELSE (io_stall_read_ms/num_of_reads)

       END AS [Read Latency],

       CASE

              WHEN io_stall_write_ms = 0 THEN 0

              ELSE (io_stall_write_ms/num_of_writes)

       END AS [Write Latency],

       CASE

              WHEN (num_of_reads = 0 AND num_of_writes = 0) THEN 0

              ELSE (io_stall/(num_of_reads + num_of_writes))

       END AS [Overall Latency],

       CASE

              WHEN num_of_reads = 0 THEN 0

              ELSE (num_of_bytes_read/num_of_reads)

       END AS [Avg Bytes/Read],

       CASE

              WHEN io_stall_write_ms = 0 THEN 0

              ELSE (num_of_bytes_written/num_of_writes)

       END AS [Avg Bytes/Write],

       CASE

              WHEN (num_of_reads = 0 AND num_of_writes = 0) THEN 0

              ELSE ((num_of_bytes_read + num_of_bytes_written)/(num_of_reads + num_of_writes))

       END AS [Avg Bytes/Transfer]

FROM (SELECT LEFT(mf.physical_name, 2) AS Drive, SUM(num_of_reads) AS num_of_reads,

                SUM(io_stall_read_ms) AS io_stall_read_ms, SUM(num_of_writes) AS num_of_writes,

                SUM(io_stall_write_ms) AS io_stall_write_ms, SUM(num_of_bytes_read) AS num_of_bytes_read,

                SUM(num_of_bytes_written) AS num_of_bytes_written, SUM(io_stall) AS io_stall

      FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS vfs

      INNER JOIN sys.master_files AS mf WITH (NOLOCK)

      ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id

      GROUP BY LEFT(mf.physical_name, 2)) AS tab

ORDER BY [Overall Latency] OPTION (RECOMPILE);


信息提取:



各數據文件IO/CPU/Buffer訪問狀況,Temp DB的IO Rank達到53%以上

 


解決方案:


添加多個Temp DB 數據文件,減小Latch爭用。最佳實踐:>8核數,建議添加4個或8個等大小的數據文件。


其餘等待


分析:


經過等待類型發現與IO相關 的PAGEIOLATCH_XX 值很是高,數據庫存存在大量表掃描操做,致使緩存中數據不能知足查詢,須要從磁盤中讀取數據,產生IO等待。


解決方案:


建立合理非彙集索引來減小掃描,更新統計信息


上面方法還沒法解決,考慮將受影響的數據轉移到更快的IO子系統,考慮增長內存。


4、優化方案


依據以上監測和分析結果,從「優化順序」和「實施原則」開始實質性的優化。


優化順序


1.    從數據庫配置優化


理由:代價最小,根據監測分析結果,經過修改配置可提高空間不小。


2.    索引優化


理由:索引不會動數據庫表等與業務緊密的結構,業務層面不會有風險。


步驟:考慮到庫中打表(超過100G),在索引優化也要分步進行。 優化索引步驟:無用索引->重複索引->丟失索引添加->彙集索引->索引碎片整理。


3.    查詢優化


理由:語句優化須要結合業務,須要和開發人員緊密溝通,最終選擇優化語句的方案


步驟:DBA抓取執行時間、使用CPU、IO、內存最多的TOP SQL語句/存儲過程,交由開發人員並協助找出可優化的方法,如加索引、語句寫法等。


實施原則


整個診斷和優化方案首先在測試環境中進行測試,將在測試環境中測試經過並確認的逐步實施到正式環境。


數據庫配置優化


1. 當前數據庫服務器有超過24個核數, 當前MAXDOP爲0,配置不合理,致使調度併發處理時出現較大並行等待現象(水桶短板原理)


優化建議:建議修改MAXDOP 值,最佳實踐>8核的,先設置爲4


2. 當前COST THRESHOLD FOR PARALLELISM值默認5秒


優化建議:建議修改 COST THRESHOLD FOR PARALLELISM值,超過15秒容許並行處理


3. 監測到業務中處理用頻繁使用臨時表、標量值函數,不斷建立用戶對象等,TEMPDB 處理內存相關PFSGAMSGAM時,有不少的Latch等待現象,給性能形成影響


優化建議:建議添加多個Temp DB 數據文件,減小Latch爭用。最佳實踐:>8核數,建議添加4個或8個等大小的數據文件。


4. 啓用optimize for ad hoc workloads


5. Ad Hoc Distributed Queries開啓即席查詢優化


索引優化


1. 無用索引優化


目前庫中存在大量無用索引,可經過腳本找出無用的索引並刪除,減小系統對索引維護成本,提升更新性能。另外,根據讀比率低於1%的表的索引,可結合業務最終確認是否刪除索引。


詳細列表請參考:性能調優數據收集_索引.xlsx-無用索引


無用索引,參考執行語句:


SELECT  OBJECT_NAME(i.object_id) AS table_name ,

        COALESCE(i.name, SPACE(0)) AS index_name ,

        ps.partition_number ,

        ps.row_count ,

        CAST(( ps.reserved_page_count * 8 ) / 1024. AS DECIMAL(12, 2)) AS size_in_mb ,

        COALESCE(ius.user_seeks, 0) AS user_seeks ,

        COALESCE(ius.user_scans, 0) AS user_scans ,

        COALESCE(ius.user_lookups, 0) AS user_lookups ,

        i.type_desc

FROM    sys.all_objects t

        INNER JOIN sys.indexes i ON t.object_id = i.object_id

        INNER JOIN sys.dm_db_partition_stats ps ON i.object_id = ps.object_id

                                                   AND i.index_id = ps.index_id

        LEFT OUTER JOIN sys.dm_db_index_usage_stats ius ON ius.database_id = DB_ID()

                                                           AND i.object_id = ius.object_id

                                                           AND i.index_id = ius.index_id

WHERE   i.type_desc NOT IN ( 'HEAP', 'CLUSTERED' )

        AND i.is_unique = 0

        AND i.is_primary_key = 0

        AND i.is_unique_constraint = 0

        AND COALESCE(ius.user_seeks, 0) <= 0

        AND COALESCE(ius.user_scans, 0) <= 0

        AND COALESCE(ius.user_lookups, 0) <= 0

ORDER BY OBJECT_NAME(i.object_id) ,

        i.name

 

 

    --1. Finding unused non-clustered indexes.

 

    SELECT OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName ,

    OBJECT_NAME(i.object_id) AS TableName ,

    i.name ,

    ius.user_seeks ,

    ius.user_scans ,

    ius.user_lookups ,

    ius.user_updates

    FROM sys.dm_db_index_usage_stats AS ius

    JOIN sys.indexes AS i ON i.index_id = ius.index_id

    AND i.object_id = ius.object_id

    WHERE ius.database_id = DB_ID()

    AND i.is_unique_constraint = 0 -- no unique indexes

    AND i.is_primary_key = 0

    AND i.is_disabled = 0

    AND i.type > 1 -- don't consider heaps/clustered index

    AND ( ( ius.user_seeks + ius.user_scans +

    ius.user_lookups ) < ius.user_updates

    OR ( ius.user_seeks = 0

    AND ius.user_scans = 0

    )

    )


表的讀寫比,參考執行語句


DECLARE @dbid int

SELECT @dbid = db_id()

SELECT TableName = object_name(s.object_id),

       Reads = SUM(user_seeks + user_scans + user_lookups), Writes =SUM(user_updates),CONVERT(BIGINT,SUM(user_seeks + user_scans + user_lookups))*100/(SUM(user_updates)+SUM(user_seeks + user_scans + user_lookups))

FROM sys.dm_db_index_usage_stats AS s

INNER JOIN sys.indexes AS i

ON s.object_id = i.object_id

AND i.index_id = s.index_id

WHERE objectproperty(s.object_id,'IsUserTable') = 1

AND s.database_id = @dbid

GROUP BY object_name(s.object_id)

ORDER BY writes DESC


2. 移除、合併重複索引


目前系統中不少索引重複,對該類索引進行合併,減小索引的維護成本,從而提高更新性能。


重複索引,參考執行語句:


WITH MyDuplicate AS (SELECT  

Sch.[name] AS SchemaName,

Obj.[name] AS TableName,

Idx.[name] AS IndexName,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 1) AS Col1,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 2) AS Col2,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 3) AS Col3,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 4) AS Col4,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 5) AS Col5,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 6) AS Col6,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 7) AS Col7,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 8) AS Col8,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 9) AS Col9,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 10) AS Col10,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 11) AS Col11,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 12) AS Col12,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 13) AS Col13,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 14) AS Col14,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 15) AS Col15,

INDEX_Col(Sch.[name] + '.' + Obj.[name], Idx.index_id, 16) AS Col16

FROM sys.indexes Idx

INNER JOIN sys.objects Obj ON Idx.[object_id] = Obj.[object_id]

INNER JOIN sys.schemas Sch ON Sch.[schema_id] = Obj.[schema_id]

WHERE index_id > 0 AND  Obj.[name]='DOC_INVPLU')

SELECT    MD1.SchemaName, MD1.TableName, MD1.IndexName,

  MD2.IndexName AS OverLappingIndex,

  MD1.Col1, MD1.Col2, MD1.Col3, MD1.Col4,

  MD1.Col5, MD1.Col6, MD1.Col7, MD1.Col8,

  MD1.Col9, MD1.Col10, MD1.Col11, MD1.Col12,

  MD1.Col13, MD1.Col14, MD1.Col15, MD1.Col16

FROM MyDuplicate MD1

INNER JOIN MyDuplicate MD2 ON MD1.tablename = MD2.tablename

AND MD1.indexname <> MD2.indexname

AND MD1.Col1 = MD2.Col1

AND (MD1.Col2 IS NULL OR MD2.Col2 IS NULL OR MD1.Col2 = MD2.Col2)

AND (MD1.Col3 IS NULL OR MD2.Col3 IS NULL OR MD1.Col3 = MD2.Col3)

AND (MD1.Col4 IS NULL OR MD2.Col4 IS NULL OR MD1.Col4 = MD2.Col4)

AND (MD1.Col5 IS NULL OR MD2.Col5 IS NULL OR MD1.Col5 = MD2.Col5)

AND (MD1.Col6 IS NULL OR MD2.Col6 IS NULL OR MD1.Col6 = MD2.Col6)

AND (MD1.Col7 IS NULL OR MD2.Col7 IS NULL OR MD1.Col7 = MD2.Col7)

AND (MD1.Col8 IS NULL OR MD2.Col8 IS NULL OR MD1.Col8 = MD2.Col8)

AND (MD1.Col9 IS NULL OR MD2.Col9 IS NULL OR MD1.Col9 = MD2.Col9)

AND (MD1.Col10 IS NULL OR MD2.Col10 IS NULL OR MD1.Col10 = MD2.Col10)

AND (MD1.Col11 IS NULL OR MD2.Col11 IS NULL OR MD1.Col11 = MD2.Col11)

AND (MD1.Col12 IS NULL OR MD2.Col12 IS NULL OR MD1.Col12 = MD2.Col12)

AND (MD1.Col13 IS NULL OR MD2.Col13 IS NULL OR MD1.Col13 = MD2.Col13)

AND (MD1.Col14 IS NULL OR MD2.Col14 IS NULL OR MD1.Col14 = MD2.Col14)

AND (MD1.Col15 IS NULL OR MD2.Col15 IS NULL OR MD1.Col15 = MD2.Col15)

AND (MD1.Col16 IS NULL OR MD2.Col16 IS NULL OR MD1.Col16 = MD2.Col16)

ORDER BY

MD1.SchemaName,MD1.TableName,MD1.IndexName


3. 添加丟失索引


根據對語句的頻次,表中讀寫比,結合業務對缺失的索引進行創建。


丟失索引,參考執行語句:





4. 索引碎片整理


須要經過DBCC check完成索引碎片清理,提升查詢時效率。


備註:當前據庫不少表比較大(>50G),作表上索引可能花費很長時間,通常1個T的庫要8小時以上,建議制定一個詳細計劃,以表爲單位逐步碎片清理。


索引碎片參考執行語句:


SELECT '[' + DB_NAME() + '].[' + OBJECT_SCHEMA_NAME(ddips.[object_id],

DB_ID()) + '].['

OBJECT_NAME(ddips.[object_id], DB_ID()) + ']' AS [statement] ,

i.[name] AS [index_name] ,

ddips.[index_type_desc] ,

ddips.[partition_number] ,

ddips.[alloc_unit_type_desc] ,

ddips.[index_depth] ,

ddips.[index_level] ,

CAST(ddips.[avg_fragmentation_in_percent] AS SMALLINT)

AS [avg_frag_%] ,

CAST(ddips.[avg_fragment_size_in_pages] AS SMALLINT)

AS [avg_frag_size_in_pages] ,

ddips.[fragment_count] ,

ddips.[page_count]

FROM sys.dm_db_index_physical_stats(DB_ID(), NULL,

NULL, NULL, 'limited') ddips

INNER JOIN sys.[indexes] i ON ddips.[object_id] = i.[object_id]

AND ddips.[index_id] = i.[index_id]

WHERE ddips.[avg_fragmentation_in_percent] > 15

AND ddips.[page_count] > 500

ORDER BY ddips.[avg_fragmentation_in_percent] ,

OBJECT_NAME(ddips.[object_id], DB_ID()) ,

i.[name]


5. 審查沒有彙集、主鍵索引的表


當前庫不少表沒有彙集索引,須要細查緣由是否是業務要求,若是沒有特殊緣由能夠加上。


查詢語句優化


1.  從數據庫歷史保存信息中,經過DMV獲取 

相關文章
相關標籤/搜索