Sql Server索引基本知識篇

一、基本概念

1.數據的讀取

  頁(page)是SQL SERVER可以讀寫的最小I/O單位。即使只需訪問一行,也要把整個頁加載到緩存之中,再從緩存中讀取數據。物理讀取是從磁盤上讀取,邏輯讀取是從緩存中讀取。物理讀取一頁的開銷要比邏輯讀取一頁的要大得多。

SET STATISTICS IO ON

--do something...

SET STATISTICS IO OFF

可以用以上代碼來查看IO訪問情況

2.表的組織方式

  表有兩種組織方式,B樹(Balance Tree)或者堆(Heap)。當在表上創建了一個聚集索引的時候,整個表數據就以B樹的結構排列。否則就是按照堆的結構排列。無論表是怎麼組織的,都可以在表上面創建多個非聚集索引。非聚集索引都是以B樹的結構排列。

2.1 堆(Heap)

  之所以這個結構稱爲堆,是因爲它不以任何人爲指定的邏輯順序進行排列。而是按照分區組隊數據進行組織。也就是說,是按照磁盤的物理順序。只要需要讀取的數據文件沒有文件系統碎片(注意和下面提到的索引的碎片區分),這個讀取過程在磁盤中就可以連續的進行,沒有多餘的磁盤臂移動。而磁盤臂移動是I/O操作中開銷最大的操作。

  堆使用一個bitmap結構來管理數據的分配。也就是它會告訴你兩個結果,這個區是分配了,還是沒有分配。每一個區中的物理順序如下圖。

 

  對於新插入的數據,堆只管在最後一條數據的後面的一個空閒位置保存新插入的數據,不保持任何的邏輯順序。比如拿order表舉例,如果先插入orderid 4,5,6, 假設在位置1:176、 1:177、1:178這三個位置。這時再插入1,這時保存的數據就變爲4,5,6,1,  1保存在 1:179的位置。

 

2.2聚集索引(Clustered Index)

  該索引中鍵值的邏輯順序決定了表中相應行的物理順序。 
  聚集索引確定表中數據的物理順序。聚集索引類似於電話簿,後者按姓氏排列數據。由於聚集索引規定數據在表中的物理存儲順序,因此一個表只能包含一個聚集索引。但該索引可以包含多個列(組合索引),就像電話簿按姓氏和名字進行組織一樣。 
     
  聚集索引對於那些經常要搜索範圍值的列特別有效。使用聚集索引找到包含第一個值的行後,便可以確保包含後續索引值的行在物理相鄰。例如,如果應用程序執行 的一個查詢經常檢索某一日期範圍內的記錄,則使用聚集索引可以迅速找到包含開始日期的行,然後檢索表中所有相鄰的行,直到到達結束日期。這樣有助於提高此 類查詢的性能。同樣,如果對從表中檢索的數據進行排序時經常要用到某一列,則可以將該表在該列上聚集(物理排序),避免每次查詢該列時都進行排序,從而節 省成本。 

  當索引值唯一時,使用聚集索引查找特定的行也很有效率。例如,使用唯一僱員 ID 列 emp_id 查找特定僱員的最快速的方法,是在 emp_id 列上創建聚集索引或 PRIMARY KEY 約束。

  繼續拿Order表舉例,Order表中的全部數據都保存在B樹中的葉層(leaf level)中,其他層只是起到一個索引的作用,並不包含任何數據。葉層是一個雙向鏈表結構,並按照聚集索引的主鍵的邏輯順序排列。因此邏輯順序是用指針來維護。

 

2.2.1 索引碎片

數據庫中之所以會出現碎片,是因爲B樹的頁拆分造成的。具體頁拆分請參考數據結構,這裏要說的是由於拆分所產生的新頁不保證一定就會在被拆分的頁的後面,而是可能出於文件的任何位置。這就是無序頁”。換句話說,也就是在列表中處於後面位置的元素,在物理文件中卻排在前面。如果你明白指針的定義的話,這句話並不難理解。因爲葉層的雙向列表就是以指針來維護邏輯順序。

因此在按邏輯順序讀取的時候,由於無序頁的存在,可能造成磁臂頻繁的擺動。別忘記,磁盤擺動是I/O中開銷最大的操作。而I/O往往是一個系統的瓶頸所在。

如果按照物理順序來讀取,也就是unordered讀取,就會避免上面所產生的問題。再次強調,unordered是指不按邏輯順序讀取,所以叫unordered。

2.2.2 索引的層數

索引的層數,也就是B樹的高度,直接表明了一次查找操作在頁面讀取方面的開銷。一些執行計劃如Nested loop聯接會多次調用查找操作。因此理解這個概念很重要。

樹的高度主要和以下幾個因素相關

1.表的總行數。

2.平均一行保存數據的大小。

3.頁的平均密度。因爲不是每一頁都應該填充滿數據,這樣可以減少頁拆分的次數。

4.一頁所能容納的行數。

具體公式也很簡單,3級索引大概能容納4百萬行,4級索引大概能容納4億行數據。因此通常一張表的索引層數通常爲3到4級。 

 

2.3非聚集索引(NonClustered Index)

  該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同。 

  索引是通過二叉樹的數據結構來描述的,我們可以這麼理解聚簇索引:索引的葉節點就是數據節點。而非聚簇索引的葉節點仍然是索引節點,只不過有一個指針指向對應的數據塊。

  和聚集索引的區別就在於它的葉層並不包含所有的數據。在默認情況下它只包含了鍵列的數據,幷包含了一個行定位符(row locator)。這個行定位符的具體內容取決於它建立在以堆形式的表還是以B樹組織的表,換句話說也就是這張表是否建立了聚集索引會影響到非聚集索引的行定位符。如果是建立了聚集索引,那麼這個行定位符就是一個聚集鍵,我們通過這個聚集鍵再次查找聚集索引上的數據。

 

2.3.1 如果非聚集索引包含了我們需要查找的所有數據

  這種情況我們通常叫做索引覆蓋。

  正因爲非聚集索引有着和索引一樣的結構,並且由於非聚集索引所包含的列少,因此數據量就小,使得葉層的一頁能包含更多的行,因此進行一次I/O頁讀取的動作的時候,就能讀取進更多的行。因此查找效率是最高的。

  舉個不恰當的例子,美女徵婚,應徵人員的個人信息表有 “姓名、 德、 智、 體 、美、 勞、 高、 富、 帥”這幾列,按姓名排序。美女只關注“高、 富、 帥”這三列的內容,爲了更快的篩選,我們幫美女按照個人信息表的內容重新制作了一張表,這張表忽略了其他信息,只保留了高、富、帥和姓名,篩選效率當然就比原來關注更多內容時要高。

 

2.3.2 如果非聚集索引不包含我們需要查找的所有數據

  通俗的說這時我們就需要從非聚集索引中所包含的線索去包含所有數據的表中去找。

  按照我們之前的定義換句話來說,就是通過非聚集索引中的行定位符去聚集索引或者堆中去查找所需的數據。

 

二、深入淺出理解索引結構

  實際上,您可以把索引理解爲一種特殊的目錄。微軟的SQL SERVER提供了兩種索引:聚集索引(clustered index,也稱聚類索引、簇集索引)和非聚集索引(nonclustered index,也稱非聚類索引、非簇集索引)。下面,我們舉例來說明一下聚集索引和非聚集索引的區別:
  其實,我們的漢語字典的正文本身就是一個聚集索引。比如,我們要查“安”字,就會很自然地翻開字典的前幾頁,因爲“安”的拼音是“an”,而按照拼音排序漢字的字典是以英文字母“a”開頭並以“z”結尾的,那麼“安”字就自然地排在字典的前部。如果您翻完了所有以“a”開頭的部分仍然找不到這個字,那麼就說明您的字典中沒有這個字;同樣的,如果查“張”字,那您也會將您的字典翻到最後部分,因爲“張”的拼音是“zhang”。也就是說,字典的正文部分本身就是一個目錄,您不需要再去查其他目錄來找到您需要找的內容。我們把這種正文內容本身就是一種按照一定規則排列的目錄稱爲“聚集索引”。
  如果您認識某個字,您可以快速地從自動中查到這個字。但您也可能會遇到您不認識的字,不知道它的發音,這時候,您就不能按照剛纔的方法找到您要查的字,而需要去根據“偏旁部首”查到您要找的字,然後根據這個字後的頁碼直接翻到某頁來找到您要找的字。但您結合“部首目錄”和“檢字表”而查到的字的排序並不是真正的正文的排序方法,比如您查“張”字,我們可以看到在查部首之後的檢字表中“張”的頁碼是672頁,檢字表中“張”的上面是“馳”字,但頁碼卻是63頁,“張”的下面是“弩”字,頁面是390頁。很顯然,這些字並不是真正的分別位於“張”字的上下方,現在您看到的連續的“馳、張、弩”三字實際上就是他們在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我們可以通過這種方式來找到您所需要的字,但它需要兩個過程,先找到目錄中的結果,然後再翻到您所需要的頁碼。我們把這種目錄純粹是目錄,正文純粹是正文的排序方式稱爲“非聚集索引”。
  通過以上例子,我們可以理解到什麼是“聚集索引”和“非聚集索引”。進一步引申一下,我們可以很容易的理解:每個表只能有一個聚集索引,因爲目錄只能按照一種方法進行排序。

1、何時使用聚集索引或非聚集索引

下面的表總結了何時使用聚集索引或非聚集索引(很重要):

 

動作描述

使用聚集索引

使用非聚集索引

列經常被分組排序

返回某範圍內的數據

不應

一個或極少不同值

不應

不應

小數目的不同值

不應

大數目的不同值

不應

頻繁更新的列

不應

外鍵列

主鍵列

頻繁修改索引列

不應



      事實上,我們可以通過前面聚集索引和非聚集索引的定義的例子來理解上表。如:返回某範圍內的數據一項。比如您的某個表有一個時間列,恰好您把聚合索引建立在了該列,這時您查詢2004年1月1日至2004年10月1日之間的全部數據時,這個速度就將是很快的,因爲您的這本字典正文是按日期進行排序的,聚類索引只需要找到要檢索的所有數據中的開頭和結尾數據即可;而不像非聚集索引,必須先查到目錄中查到每一項數據對應的頁碼,然後再根據頁碼查到具體內容。

2、結合實際,談索引使用的誤區

      理論的目的是應用。雖然我們剛纔列出了何時應使用聚集索引或非聚集索引,但在實踐中以上規則卻很容易被忽視或不能根據實際情況進行綜合分析。下面我們將根據在實踐中遇到的實際問題來談一下索引使用的誤區,以便於大家掌握索引建立的方法。

     2.1、主鍵就是聚集索引
      這種想法筆者認爲是極端錯誤的,是對聚集索引的一種浪費。雖然SQL SERVER默認是在主鍵上建立聚集索引的。
      通常,我們會在每個表中都建立一個ID列,以區分每條數據,並且這個ID列是自動增大的,步長一般爲1。我們的這個辦公自動化的實例中的列Gid就是如此。此時,如果我們將這個列設爲主鍵,SQL SERVER會將此列默認爲聚集索引。這樣做有好處,就是可以讓您的數據在數據庫中按照ID進行物理排序,但筆者認爲這樣做意義不大。
      顯而易見,聚集索引的優勢是很明顯的,而每個表中只能有一個聚集索引的規則,這使得聚集索引變得更加珍貴。
      從我們前面談到的聚集索引的定義我們可以看出,使用聚集索引的最大好處就是能夠根據查詢要求,迅速縮小查詢範圍,避免全表掃描。在實際應用中,因爲 ID號是自動生成的,我們並不知道每條記錄的ID號,所以我們很難在實踐中用ID號來進行查詢。這就使讓ID號這個主鍵作爲聚集索引成爲一種資源浪費。其次,讓每個ID號都不同的字段作爲聚集索引也不符合“大數目的不同值情況下不應建立聚合索引”規則;當然,這種情況只是針對用戶經常修改記錄內容,特別是索引項的時候會負作用,但對於查詢速度並沒有影響。
      在辦公自動化系統中,無論是系統首頁顯示的需要用戶簽收的文件、會議還是用戶進行文件查詢等任何情況下進行數據查詢都離不開字段的是“日期”還有用戶本身的“用戶名”。
      通常,辦公自動化的首頁會顯示每個用戶尚未簽收的文件或會議。雖然我們的where語句可以僅僅限制當前用戶尚未簽收的情況,但如果您的系統已建立了很長時間,並且數據量很大,那麼,每次每個用戶打開首頁的時候都進行一次全表掃描,這樣做意義是不大的,絕大多數的用戶1個月前的文件都已經瀏覽過了,這樣做只能徒增數據庫的開銷而已。事實上,我們完全可以讓用戶打開系統首頁時,數據庫僅僅查詢這個用戶近3個月來未閱覽的文件,通過“日期”這個字段來限制表掃描,提高查詢速度。如果您的辦公自動化系統已經建立的2年,那麼您的首頁顯示速度理論上將是原來速度8倍,甚至更快。
      在這裏之所以提到“理論上”三字,是因爲如果您的聚集索引還是盲目地建在ID這個主鍵上時,您的查詢速度是沒有這麼高的,即使您在“日期”這個字段上建立的索引(非聚合索引)。下面我們就來看一下在1000萬條數據量的情況下各種查詢的速度表現(3個月內的數據爲25萬條):

    (1)僅在主鍵上建立聚集索引,並且不劃分時間段:

    Select gid,fariqi,neibuyonghu,title from tgongwen

    用時:128470毫秒(即:128秒)

    (2)在主鍵上建立聚集索引,在fariq上建立非聚集索引:

    select gid,fariqi,neibuyonghu,title from Tgongwen
    where fariqi> dateadd(day,-90,getdate())

    用時:53763毫秒(54秒)

    (3)將聚合索引建立在日期列(fariqi)上:

    select gid,fariqi,neibuyonghu,title from Tgongwen
    where fariqi> dateadd(day,-90,getdate())

    用時:2423毫秒(2秒)

      雖然每條語句提取出來的都是25萬條數據,各種情況的差異卻是巨大的,特別是將聚集索引建立在日期列時的差異。事實上,如果您的數據庫真的有1000 萬容量的話,把主鍵建立在ID列上,就像以上的第1、2種情況,在網頁上的表現就是超時,根本就無法顯示。這也是我摒棄ID列作爲聚集索引的一個最重要的因素。得出以上速度的方法是:在各個select語句前加:

    declare @d datetime
    set @d=getdate()

    並在select語句後加:

    select [語句執行花費時間(毫秒)]=datediff(ms,@d,getdate())

    2.2、只要建立索引就能顯著提高查詢速度
      事實上,我們可以發現上面的例子中,第2、3條語句完全相同,且建立索引的字段也相同;不同的僅是前者在fariqi字段上建立的是非聚合索引,後者在此字段上建立的是聚合索引,但查詢速度卻有着天壤之別。所以,並非是在任何字段上簡單地建立索引就能提高查詢速度。
      從建表的語句中,我們可以看到這個有着1000萬數據的表中fariqi字段有5003個不同記錄。在此字段上建立聚合索引是再合適不過了。在現實中,我們每天都會發幾個文件,這幾個文件的發文日期就相同,這完全符合建立聚集索引要求的:“既不能絕大多數都相同,又不能只有極少數相同”的規則。由此看來,我們建立“適當”的聚合索引對於我們提高查詢速度是非常重要的。

    2.3、把所有需要提高查詢速度的字段都加進聚集索引,以提高查詢速度
      上面已經談到:在進行數據查詢時都離不開字段的是“日期”還有用戶本身的“用戶名”。既然這兩個字段都是如此的重要,我們可以把他們合併起來,建立一個複合索引(compound index)。
      很多人認爲只要把任何字段加進聚集索引,就能提高查詢速度,也有人感到迷惑:如果把複合的聚集索引字段分開查詢,那麼查詢速度會減慢嗎?帶着這個問題,我們來看一下以下的查詢速度(結果集都是25萬條數據):(日期列fariqi首先排在複合聚集索引的起始列,用戶名neibuyonghu排在後列):

    (1)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>''2004-5-5''

    查詢速度:2513毫秒

    (2)select gid,fariqi,neibuyonghu,title from Tgongwen
                where fariqi>''2004-5-5'' and neibuyonghu=''辦公室''

    查詢速度:2516毫秒

    (3)select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu=''辦公室''

    查詢速度:60280毫秒

      從以上試驗中,我們可以看到如果僅用聚集索引的起始列作爲查詢條件和同時用到複合聚集索引的全部列的查詢速度是幾乎一樣的,甚至比用上全部的複合索引列還要略快(在查詢結果集數目一樣的情況下);而如果僅用複合聚集索引的非起始列作爲查詢條件的話,這個索引是不起任何作用的。當然,語句1、2的查詢速度一樣是因爲查詢的條目數一樣,如果複合索引的所有列都用上,而且查詢結果少的話,這樣就會形成“索引覆蓋”,因而性能可以達到最優。同時,請記住:無論您是否經常使用聚合索引的其他列,但其前導列一定要是使用最頻繁的列。

3、其他書上沒有的索引使用經驗總結

     3.1、用聚合索引比用不是聚合索引的主鍵速度快
      下面是實例語句:(都是提取25萬條數據)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''

    使用時間:3326毫秒

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000

    使用時間:4470毫秒

    這裏,用聚合索引比用不是聚合索引的主鍵速度快了近1/4。

     3.2、用聚合索引比用一般的主鍵作order by時速度快,特別是在小數據量情況下

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi

    用時:12936

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid

    用時:18843

      這裏,用聚合索引比用一般的主鍵作order by時,速度快了3/10。事實上,如果數據量很小的話,用聚集索引作爲排序列要比使用非聚集索引速度快得明顯的多;而數據量如果很大的話,如10萬以上,則二者的速度差別不明顯。

     3.3、使用聚合索引內的時間段,搜索時間會按數據佔整個數據表的百分比成比例減少,而無論聚合索引使用了多少個:

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>''2004-1-1''

    用時:6343毫秒(提取100萬條)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>''2004-6-6''

    用時:3170毫秒(提取50萬條)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''

    用時:3326毫秒(和上句的結果一模一樣。如果採集的數量一樣,那麼用大於號和等於號是一樣的)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
                where fariqi>''2004-1-1'' and fariqi<''2004-6-6''

    用時:3280毫秒

     3.4、日期列不會因爲有分秒的輸入而減慢查詢速度
      下面的例子中,共有100萬條數據,2004年1月1日以後的數據有50萬條,但只有兩個不同的日期,日期精確到日;之前有數據50萬條,有5000個不同的日期,日期精確到秒。

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
              where fariqi>''2004-1-1'' order by fariqi

    用時:6390毫秒

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
                where fariqi<''2004-1-1'' order by fariqi

    用時:6453毫秒

4、其他注意事項

      “水可載舟,亦可覆舟”,索引也一樣。索引有助於提高檢索性能,但過多或不當的索引也會導致系統低效。因爲用戶在表中每加進一個索引,數據庫就要做更多的工作。過多的索引甚至會導致索引碎片。
      所以說,我們要建立一個“適當”的索引體系,特別是對聚合索引的創建,更應精益求精,以使您的數據庫能得到高性能的發揮。

 

轉載自:https://www.cnblogs.com/taiyonghai/p/5780907.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章