sqlserver調優-索引


話題背景,前幾天一個同事,碰到一個問題,說hangfire有個坑,丟任務。我當時很驚訝,就回了一句基本不會丟任務,除非hangfire退出服務或者任務非常多,並且hangfire數據庫延時比較大的情況下才會發生。當時我在處理別的問題,也就沒有跟他一起去看這個問題。待手上的任務完成之后,我就順便過去跟他一起看了下代碼(因為我也想看看到底是不是hangfire的鍋),看了他的代碼之后,我們通過簡單的分析,把這個問題的源頭定在了mysql數據庫。可能說的有點無厘頭,我簡單描述下整個業務場景吧。這里簡單分為四個角色,hangfire rabbitmq consumer mysql,而它們之間的關系是hangfire定時寫任務到mq,mq推消息到consumer(mq為自動確認機制),consumer通過一系列的業務操作寫入mysql。整個操作的源頭是hangfire,所以自然很容易就想到hangfire是不是丟任務或者不執行了?其實這個問題,只要hangfire和mq不宕機,應該就是consumer的鍋了,當時是測試同事在做壓測,后面我們仔細看代碼發現,consumer里面的業務處理還是比較復雜的,各種查、添和改操作,又因為mq自動確認,所以consumer由於處理數據過大導致數據庫高延時而最終數據庫性能急劇下降。
 
索引在MSSQL數據庫里面有兩種索引,聚集和非聚集兩種,我們先看下聚集索引。
 
聚集索引聚集索引是指數據庫表行中數據的物理順序與鍵值的邏輯(索引)順序相同。一個表只能有一個聚集索引,因為一個表的物理順序只有一種情況,所以,對應的聚集索引只能有一個。如果某索引不是聚集索引,則表中的行物理順序與索引順序不匹配,與非聚集索引相比,聚集索引有着更快的檢索速度(來源百度百科)。其實現方式是通過B+樹結構實現,B+樹這種數據結構,百度有很多,大家討論的無非就兩點,特點和增刪改查。它就是一顆平衡多路搜索樹,如果是剛接觸它,你可以按二叉平衡樹的方式去理解,更通俗一點的理解就是我們用的詞典,拼音首字母就可以理解為聚集索引。注意B+樹的數據是在葉子節點各葉子節點通過鏈表有序存放,而節點存儲的是聚集索引列的值。
 
非聚集索引該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同(來源百度百科)。非聚集索引同樣基於B樹實現,非聚集索引的葉層不包含數據頁。 相反,葉節點包含索引行。每個索引行包含非聚集鍵值以及一個或多個行定位器,這些行定位器指向有該鍵值的數據行(如果索引不唯一,則可能是多行)。下面我們通過實例來說吧。
我這邊的實例就圖個方便,用的是hangfire的表,數據是自己造的,三張關系表如下:
如上就是三張關系表的基本接結構,字段不多,我們先從job表開始吧,這張表我已經把聚集和非聚集索引全部刪除掉了,就只留下一個主鍵。我們先看下最簡單的查詢,沒有where條件的,看看執行計划是什么樣的。
 
單表查詢
SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
  FROM [Hangfire.Sample].[HangFire].[Job]
下面是查詢結果和執行計划
在執行計划這張表格里面比較重要的字段我有標注。
1.每個步驟實際返回的行數,2.是每個步驟實際執行的次數,3.是每個步驟預計返回的行數,4.根據io和cpu字段通過公式計算出來的cost總和(包括自己和下層步驟),這里還有一個最重要的字段stmtText,我沒有標注,他就是我們的執行計划的具體內容。指標解析就到這吧,后續會細說這些指標以及跟統計信息的關系,因為它們的值直接受統計信息影響,而它們又直接影響查詢性能。
我們現在把關注點放在執行計划內容里面吧,一個select * from 表,sqlserver直接通過表掃描查詢,Table Scan。現在我們把這張表字段id添加聚集索引,再看執行計划。
1 SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
2   FROM [Hangfire.Sample].[HangFire].[Job]
同樣的sql查詢語句,現在的執行計划內容變成了聚集索引掃描,clustered index scan,不要被index字樣迷惑了,效率是一樣的沒有區別,唯一的區別是table scan表示數據表里面沒有聚集索引。下面我們可以簡單通過底層數據結構來看。
堆表這樣展示更直觀吧
聚集索引掃描
下面我們在where里面添加條件,條件字段為聚集索引字段,看看具體效果。
1 SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
2   FROM [Hangfire.Sample].[HangFire].[Job] where id = 16251
3 SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
4   FROM [Hangfire.Sample].[HangFire].[Job] where id in (16251,16252,16253)
兩條查詢語句,第一條是id=,第二條是id in,具體看計划
我們直接看計划內容,兩條查詢語句都使用了聚集索引seek查找,其實也很容易理解,就是通過B+樹的查找實現,下面我們看下<>操作。
這個執行計划比上面的要稍微復雜點,我先簡單說下怎么看這個執行計划。執行計划其實就是一顆樹,執行的順序是從里到外,同層次的節點是從上到下。在這個執行計划里面先是根據用戶輸入構造常量行列再合並-》再通過聚集索引查找seek數據,執行了兩次,sqlserver很聰明,其實際就是把<>分為>和<執行,-》最后再nested loops並且把常量行作為outer,然后輸出表格結果。整個計划的執行大概就是這樣的,上面的查詢計划里面有幾個出現次數比較多的操作符,我大概簡單介紹下。1.Constant Scan翻譯過來為常量掃描運算符,其實際就是生成數據行但是當前沒有列,2.Compute Scalar 標量計算操作符,它主要是計算表達式值(你可以看后面的define定義表達式)可以返回輸出也可以提供給其他計划查詢,10行和9行的意思就是先創建數據行然后通過計算表達式獲得的值填充到這個行的列。3.Concatenation為串聯操作符,可以理解union all,在這里就是和並下面的行。這里建議盡量少用<> not 等操作符。下面我們看下單表非聚集索引的執行計划。
我們為job表添加一個非聚集索引,字段stateName,下面我們通過它來查詢,看看計划到底怎么樣的。
1    SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
2   FROM [Hangfire.Sample].[HangFire].[Job] where StateName='4 Value'
執行計划
先通過非聚集索引seek到一條數據,這條數據有兩個值statename和id(聚集索引字段,因為非聚集在建索引的時候做了映射),然后通過id在聚集索引上seek到一條數據,為什么要這樣做?因為其他的檢索列不在里面,所以其他的建索列需要到聚集索引上面的葉子節點里面獲取,最后做了個loops,把兩個結果集合並起來,可以通過圖形執行計划看到兩次seek的輸出具體字段也可以看outputlist字段。
我們為job表再添加一條非聚集索引,索引字段為ExpireAt,該字段為時間類型,我們看下查詢計划是什么樣子的。
1    SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
2   FROM [Hangfire.Sample].[HangFire].[Job] where ExpireAt > '2016-05-18  14:15:43.000' and ExpireAt<'2016-05-22 02:59:19.000'
執行計划
輸出的行數是667行,還是通過索引seek,但是loops的代價有點大,做了667次。我們稍微改下查詢條件,輸出更多的行,看看計划又是什么樣的。
1    SELECT  [Id],[StateId],[StateName],[InvocationData],[Arguments],[CreatedAt],[ExpireAt]
2   FROM [Hangfire.Sample].[HangFire].[Job] where ExpireAt > '2016-05-18  14:15:43.000'
執行計划
同樣是用非聚集索引ExpireAt做where查詢,這次的執行計划就完全不同了,聚集索引scan掃描,為什么我用了非聚集索引字段怎么還是scan呢?其實這個地方如果用索引seek性能更差,會發生290805次loop,所以sqlserver直接選擇scan。為什么sqlserver會根據輸出數據量使用不同的執行計划?這個問題就是我前面說的統計信息,這部分后面再說。還有個細節問題,如果檢索字段包含在非聚集索引里面,查詢語句會直接通過索引seek輸出,當然取決於數據量,這部分就不演示,下面看看連表操作,又是什么樣子。
 
多表聯查
在說多表聯查之前我們需要先了解執行計划的三種join運算符,分別為Nested loops、Merge、Hash這三種,這三種運算算法沒有誰絕對的好,取決於輸出數據量和硬件資源。我們分別簡單介紹下這三種算法的邏輯。
loop:是一種邏輯最簡單最基本的join方式,普遍被使用。它的算法是如果兩張表,它會選擇一張為outer table,另外一張為inner table,outer table它會通過篩選條件每查出一條數據都會join到inner table里面去匹配數據行,多少數據匹配了多少次,可以通過rows和executes字段查看。其算法復雜度兩表篩選數據的乘積,由此可以看出此運算符不適合兩表數據量比較大,sqlserver會根據統計信息決定是否用哪種運算符。
merge:從兩邊的篩選數據集里面各取一個值,比較,如果相等就把這兩行連接起來返回,如果不相等就把小的值丟掉,比較的是關聯字段。注意如果某張表關聯字段是非聚集索引或者沒有索引,sqlserver會對該表執行表掃描再排序再按上面的方法merge(說明merge需要字段先排序),當然還有很多情況,可以自己去看,我說的一般是按比較好的情況去說明。
Hash:hash連接就很好理解了,充分發揮它的查詢優勢,先以某張表做為基表(不是全部數據,篩選數據),第二張表也是篩選數據依次跟基表篩選集比較,符合條件的,返回兩行數據的連接集。
先簡單說下表結構,job和jobParamter兩張關系表,關聯字段是id和jobid,主表job有36w數據,子表有3.4w左右數據,id和jobid均為聚集索引。
loop:
1    select  a.StateName, a.Id, a.ExpireAt, b.Name  from HangFire.Job a inner  join  HangFire.JobParameter b
2 on a.Id = b.JobId
3 where a.Id >30943
執行計划
耗時
兩張表均為聚集索引seek數據,首先outer table jobParameter表seek出來19056條數據,每條數據都要到inner table job表里面去匹配,這里是seek,執行了19056次,最終返回的數據也是19056條數據。下面我們執行同樣的語句,看看效果怎么樣。
1 select  a.StateName, a.Id, a.ExpireAt, b.Name  from HangFire.Job a inner  join  HangFire.JobParameter b
2 on a.Id = b.JobId
3 where a.Id >20943
執行計划
耗時
上面的sql語句我僅僅只是改了條件參數值,多返回了10000數據,總共返回了29056條數據,兩次的執行計划就完全不同了。我們具體看下執行計划,兩張表均通過聚集索引seek了29056條數據並且各執行了一次,最后做merge join操作。hash join這里就不嘗試了,同樣一句sql語句,sqlserver為什么會選擇不同的執行計划執行?這就是統計信息。
 
統計信息
統計信息可以通過sql命令DBCC SHOW_STATISTICS('表名', '索引名')查看。
1 DBCC SHOW_STATISTICS('HangFire.Job', 'PK_HangFire_Job')
第一張表就是統計信息的基本信息,rows 代表表里面的實際行數。rows sampled代表取樣信息數,如果跟rows不等表示沒有對整表掃描取樣,一般大數據表是抽樣。steps表示數據被分成了多少組,按組統計。
第二張表是統計信息的選擇評估信息表,all density表示該索引列的選擇性,就是評估這個字段作為索引的匹配度,小於0.1表示比較好,選擇性高,大於0.1反之。
第三張表表示統計信息的直方圖信息表,range_hi_key表示每一組數據的最大值,steps不是把索引字段的所有數據分成33組。range_rows表示在一個閉區間里面的行數,不包括上限值,以第一二行來說,第一行1-16254這個閉區間里面沒有數據行,第二行表示16255-65842這個閉區間有48108.16,這只是一個評估值,不是實際的。第三行eq_rows表示等於上限值的數據有多少。sqlserver結合這三張表的信息就能准確做出評估,選擇什么樣的執行計划來做查詢,我們上面的計划也是由他來評估的。注意如果統計信息過時,或者說沒有及時發生更新,這樣會導致sqlserver做出錯誤的預判,選擇不合適的執行計划,性能會大打折扣,一般情況下,注意執行計划里面的三個字段rows executes estimaterows,estimate rows字段來源於統計信息,通過它來估算io和cpu最后計算出totalcost,sqlserver根據totalcost字段值選擇合適的執行計划,下面我們看一個未建索引的字段,同時又包含在檢索列里面,看看,下面看下聚合和排序等操作。
 
聚合&排序
聚合在sqlserver的執行計划里面分兩種,stream和hash。stream聚合適合數據集是有序的(比如group by里面包含了聚集索引)或者groupby非聚集字段並且數據量不大,其他情況一般是hash聚合。看代碼。
1 select a.Arguments,count(Id)  from HangFire.Job a inner  join  HangFire.JobParameter b
2 on a.Id = b.JobId
3 where a.id >571678 and a.id < 580000
4 group by a.Arguments
執行計划
因為Arguments字段為非聚集字段,所以在stream聚合之前做了一個排序,排序完之后入有序隊列再一行一行的數據輸入去做匹配,如果匹配就更新標量值,否則開始新的group by匹配。同樣的語句我們看看hash聚合的執行計划。
1 select a.Arguments,count(Id)  from HangFire.Job a inner  join  HangFire.JobParameter b
2 on a.Id = b.JobId
3 where a.id >571678 and a.id < 680000
4 group by a.Arguments
執行計划
返回數據集有10w左右,sqlserver聚合操作選擇hash聚合,因為它評估發現,輸出的數據集有10w+數據,數據量比較大而且需要排序,所以它選擇了hash聚合操作,hash操作不需要排序,從數據集里面匹配分組數據,匹配到了就更新聚合值,需要全部檢索完。
 
一些建議
1. 盡量不要做多表聯查,尤其是三表(含)以上聯查操作,是否考慮其他設計,如冗余反范式表,倒排分片表;
2. 聚集索引列建議采用數值類型(字符串類型會導致索引重排,范圍查找),如雪花id,redis-id,充分利用B+樹檢索優勢;
3. 不要太糾結索引seek,有時候scan比seek性能更好,主要看output的行數,一般情況下數據庫會通過統計信息選擇最優的方案;
4. 其他關鍵字啥的,百度有很多。
 
 
 
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM