1. 程式人生 > >資料庫(八)

資料庫(八)

前言

本篇部落格內容為索引,索引是為了提高資料庫的查詢效率。

索引

什麼是索引?

索引就相當於書的目錄,是 mysql 中一種專門的資料結構,稱為 key,索引的本質原理就是通過不斷地縮小查詢範圍,來降低 io 次數從而提升查詢效能。強調:一旦為表建立了索引,以後的查詢都會先查索引,再根據索引定位的結果去找資料。

為什麼要用索引?

對於一個應用來說,對資料庫的讀寫比例基本上是10:1,即讀多寫少。而且對於寫來說極少出現效能問題,大多數效能問題都是慢查詢提到加速查,這時候就必須用到索引。

索引的影響

  1. 在表中有大量資料的前提下,建立索引速度會很慢;
  2. 在索引建立完畢後,對錶的查詢效能會大幅度提升,但是寫效能會降低。

磁碟 IO 與預讀

磁碟讀取資料靠的是機械運動,每次讀取資料花費的時間可以分為尋道時間、旋轉延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁軌所需要的時間,主流磁碟一般在5ms 以下;旋轉延遲就是磁碟轉速花費的時間,比如一個磁碟7200轉,表示每分鐘能轉7200次,也就是一秒鐘能轉120次,旋轉延遲就是1/120/2=4.17ms;傳輸時間指的是從磁碟讀出或將資料寫入磁碟的時間,一般在零點幾毫秒,相對於前兩個時間來說可以忽略不計。那麼訪問一次磁碟的時間,即一次磁碟 IO 的時間約等於5+4.17=9ms 左右,看起來還挺短的,但是對於現在的計算機來說,一秒鐘可以執行5億條指令,執行一次 IO 的時間可以執行約450萬條指令,資料庫動輒十萬百萬乃至千萬級別資料,每次9毫秒的時間,很顯然會造成大量的等待時間。下圖是計算機硬體延遲的對比圖:

考慮到磁碟 IO 是非常高昂的操作,計算機作業系統做了一些優化,當一次 IO 時,不光把當前地址的資料,而是把相鄰的資料也都讀取到記憶體緩衝區,因為區域性預讀性原理告訴我們,當計算機訪問一個地址的資料的時候,與其相鄰的資料也會很快被訪問到。每一次 IO 讀取的資料稱之為一頁(page)。具體一頁有多大資料跟作業系統有關,一般為4k 或8k,也就是我們讀取一頁內的資料時候才發生了一次 IO。

索引的資料結構

對於索引來說,它的目的就是降低查詢資料時產生的磁碟 IO 數量級,最好是常數數量級。索引使用的是 B+樹(B+樹是通過二叉查詢樹,再由平衡二叉樹,B 樹演化而來)。

如上圖,是一顆 B+數,淺藍色的塊稱之為一個磁塊,可以看到每個人磁塊包含幾個資料項(深藍色所示)和指標(黃色所示),如磁碟快1包含資料項17和35,包含指標 P1、P2、P3,P1表示小於17的磁碟塊,P2表示在17和35之間的磁碟塊,P3表示大於35的磁碟塊。真實資料存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點只不過儲存真實的資料,只儲存指引搜尋方向的資料項,如17、35並不真實存在於資料表中。

B+樹查詢過程

如果所示,如果要查詢資料項29,那麼首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次 IO,在記憶體中用二分法查詢確定29在17和35之間,鎖定磁碟塊1的 P2指標,記憶體時間因為非常短(相比磁碟 IO)可以忽略不計,通過磁碟塊1的 P2指標的磁碟地址把磁碟塊3由磁碟載入到記憶體,發生第三次 IO,同時記憶體中做二分查詢找到29,結束查詢,總計三次 IO。真實情況是,3層的 B+數可以表示上百萬的資料,如果上百萬的資料查詢只需要三次 IO,效能提升將是巨大的,如果沒有索引,每個資料項都要發生一次 IO,那麼總共需要百萬次的 IO,顯然成本非常非常高。

B+數性質

  1. 索引欄位要儘量的小:IO 次數取決於 B+樹的高度 h,假設當前資料表的資料為 N,每次磁碟塊的資料項的數量是 m,則有 h=log(m+1)N,當資料量 N 一定的情況下,m 越大,h 越小;而 m=磁碟塊的大小/資料項的大小,磁碟塊的大小也是一個數據頁的大小,是固定的,如果資料項佔的空間越小,資料項的數量越多,樹的高度越低。這就是為什麼每個資料項,即索引欄位要儘量的小,比如 int 佔4位元組,要比 bigint 8位元組少一半。這也是為什麼 B+樹要求把真實的資料放在葉子節點而不是內層節點,一旦放到內層節點,磁碟塊的資料項會大幅度下降,導致樹增高。當資料項等於1時將會退化成線性表。
  2. 索引的最左匹配特性:當B+樹的資料項是複合的資料結構,比如(name,age,sex)的時候,B+數是按照從左到右的順序來建立搜尋樹的,比如當(張三,20,F)這樣的資料來檢索的時候,B+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的資料;但當(20,F)這樣的沒有name的資料來的時候,B+樹就不知道下一步該查哪個節點,因為建立搜尋樹的時候name就是第一個比較因子,必須要先根據name來搜尋才能知道下一步去哪裡查詢。比如當(張三,F)這樣的資料來檢索時,B+樹可以用name來指定搜尋方向,但下一個欄位age的缺失,所以只能把名字等於張三的資料都找到,然後再匹配性別是F的資料了, 這個是非常重要的性質,即索引的最左匹配特性。

聚集索引與輔助索引

在資料庫中,B+樹的高度一般都在2~4層,這也就是說查詢某一個鍵值的行記錄時最多隻需要2到4次IO,這倒不錯。因為當前一般的機械硬碟每秒至少可以做100次IO,2~4次的IO意味著查詢時間只需要0.02~0.04秒。

資料庫中的B+樹索引可以分為聚集索引(clustered index)和輔助索引(secondary index),

聚集索引與輔助索引相同的是:不管是聚集索引還是輔助索引,其內部都是B+樹的形式,即高度是平衡的,葉子結點存放著所有的資料。

聚集索引與輔助索引不同的是:葉子節點存放的是否是一條完整的記錄。

聚集索引

InnoDB儲存引擎表示索引組織表,即表中資料按照主鍵順序存放。而聚集索引(clustered index)就是按照每張表的主鍵構造一棵B+樹,同時葉子結點存放的即為整張表的行記錄資料,也將聚集索引的葉子結點稱為資料頁。聚集索引的這個特性決定了索引組織表中資料也是索引的一部分。同B+樹資料結構一樣,每個資料頁都通過一個雙向連結串列來進行連結。
    
如果未定義主鍵,MySQL取第一個唯一索引(unique)而且只含非空列(NOT NULL)作為主鍵,InnoDB使用它作為聚簇索引。
    
如果沒有這樣的列,InnoDB就自己產生一個這樣的ID值,它有六個位元組,而且是隱藏的,使其作為聚簇索引。

由於實際的資料頁只能按照一棵B+樹進行排序,因此每張表只能擁有一個聚集索引。在這種情況下,查詢優化器傾向於採用聚集索引。因為聚集索引能夠在B+樹索引的葉子節點上直接找到資料。此外由於定義了資料的邏輯順序,聚集索引能夠特別快地訪問針對範圍值得查詢。

聚集索引優點

  1. 它對主鍵的排序查詢和範圍查詢速度非常快,葉子節點的資料就是使用者所要查詢的資料。如使用者需要查詢一張表,查詢最後的10位使用者資訊,由於 B+樹索引是雙向連結串列,所以使用者可以快速找到最後一個數據頁,並取出10條記錄。
  2. 範圍查詢,即如果要查詢某一範圍內的資料,通過葉子節點的上層節點就可以得到頁的範圍,之後直接讀取資料頁即可。

輔助索引

表中除了聚集索引外其他索引都是輔助索引,也稱為非聚集索引,與聚集索引的區別是:輔助索引的葉子節點不包含行記錄的全部資料。

葉子節點除了包含鍵值以外,每個葉子節點中的索引行中還包含一個書籤。該書籤用來告訴 InnoDB 儲存引擎去哪裡可以找到與索引相對應的行資料。

由於 InnoDB 儲存引擎是索引組織表,因此 InnoDB 儲存引擎的輔助索引的書籤就是相應行資料的聚集索引鍵。如下圖:

輔助索引的存在並不影響資料再聚集索引中的組織,因此每張表中可以有多個輔助索引,但只能有一個聚集索引。當通過輔助索引來尋找資料時,InnoDB儲存引擎會遍歷輔助索引並通過葉子級別的指標獲得指向主鍵索引的主鍵,然後通過主鍵索引來找到一個完整的行記錄。

舉例來說,如果在一棵高度為3的輔助索引樹種查詢資料,那需要對這個輔助索引樹遍歷3次找到指定主鍵,如果聚集索引樹的高度同樣為3,那麼還需要對聚集索引樹進行三次查詢,最終找到一個完整的行資料所在的頁,因此一共需要6次 IO 訪問才能得到最終的一個數據頁。

MySQL 索引管理

索引的功能

  1. 索引的功能就是加速查詢
  2. mysql 中的 primary key、unique,聯合唯一都是索引,這些索引除了加速查詢以外,還有約束的功能。

MySQL 常用索引

  • 普通索引:index 加速查詢
  • 唯一索引:
    • 主鍵索引 primary key:加速查詢+約束(不能為空,不能重複)
    • 唯一索引 unique:加速查詢+約束(不能重複)
  • 聯合索引:
    • primary key(id,name):聯合主鍵索引
    • unique(id,name):聯合唯一索引
    • index(id,name):聯合普通索引

建立/刪除索引

  1. 建立表時
mysql> create table 表名(
       欄位名1 資料型別 [完整性約束條件...],
       欄位名2 資料型別 [完整性約束條件...],
       [unique | fulltext | spatial] index | key
       [索引名] (欄位名[(長度)] [asc | desc]));
  1. 在已存在的表上建立索引
mysql> create [unique | fulltext | spatial ] index 索引名
       on 表名 (欄位名[(長度)] [asc | desc]);
  1. Alter table在已存在的表上建立索引
mysql> alter table 表名 add [unique | fulltext | spatial ] index
       索引名(欄位名[(長度)] [asc | desc]);
  1. 刪除索引
mysql> drop index 索引名 on 表名;

總結

1. 一定是為搜尋條件的欄位建立索引,比如select * from s1 where id = 333;就需要為id加上索引

2. 在表中已經有大量資料的情況下,建索引會很慢,且佔用硬碟空間,建完後查詢速度加快
比如create index idx on s1(id);會掃描表中所有的資料,然後以id為資料項,建立索引結構,存放於硬碟的表中。
建完以後,再查詢就會很快了。

3. 需要注意的是:innodb表的索引會存放於s1.ibd檔案中,而myisam表的索引則會有單獨的索引檔案table1.MYI

MySAM索引檔案和資料檔案是分離的,索引檔案僅儲存資料記錄的地址。而在innodb中,表資料檔案本身就是按照B+Tree(BTree即Balance True)組織的一個索引結構,這棵樹的葉節點data域儲存了完整的資料記錄。這個索引的key是資料表的主鍵,因此innodb表資料檔案本身就是主索引。
因為inndob的資料檔案要按照主鍵聚集,所以innodb要求表必須要有主鍵(Myisam可以沒有),如果沒有顯式定義,則mysql系統會自動選擇一個可以唯一標識資料記錄的列作為主鍵,如果不存在這種列,則mysql會自動為innodb表生成一個隱含欄位作為主鍵,這欄位的長度為6個位元組,型別為長整型.

正確使用索引

並不是說建立了索引就一定會加快查詢速度,若想利用索引達到預期的提高查詢速度的效果,我們在新增索引時,必須考慮以下問題:

  1. 範圍問題,或者說條件不明確,條件中出現這些符號:>、>=、<、<=、!=、between and、like

  2. 儘量選擇區分度高的列作為索引,區分度的公式是 count(distinct col)/count(*),表示欄位不重複的比例,比例越大我們掃描的記錄越少,唯一鍵的區分度是1,而一些狀態、性別欄位可能在資料量特別大的地方就是0,一般需要 join 的欄位要求都是0.1以上,即平均1次掃描10條記錄
  3. =和 in 可以亂序,比如 a=1 and c=3建立(a,b,c)索引可以任意順序,mysql 的查詢優化器會優化成索引可以識別的形式
  4. 索引列不能參與計算,保持列“乾淨”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很簡單,b+樹中存的都是資料表中的欄位值,但進行檢索時,需要把所有元素都應用函式才能比較,顯然成本太大。所以語句應該寫成create_time = unix_timestamp(’2014-05-29’)
  5. and/or

1、and與or的邏輯
    條件1 and 條件2:所有條件都成立才算成立,但凡要有一個條件不成立則最終結果不成立
    條件1 or 條件2:只要有一個條件成立則最終結果就成立

2、and的工作原理
    條件:
        a = 10 and b = 'xxx' and c > 3 and d =4
    索引:
        製作聯合索引(d,a,b,c)
    工作原理:
        對於連續多個and:mysql會按照聯合索引,從左到右的順序找一個區分度高的索引欄位(這樣便可以快速鎖定很小的範圍),加速查詢,即按照d—>a->b->c的順序

3、or的工作原理
    條件:
        a = 10 or b = 'xxx' or c > 3 or d =4
    索引:
        製作聯合索引(d,a,b,c)
        
    工作原理:
        對於連續多個or:mysql會按照條件的順序,從左到右依次判斷,即a->b->c->d
在左邊條件成立但是索引欄位的區分度低的情況下(name 與 gender 均屬於這種情況),會依次往右找到一個區分度高的索引欄位,加速查詢
  1. 最左字首匹配原則,非常重要的原則,對於組合索引mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就停止匹配(指的是範圍大了,有索引速度也慢),比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d的順序可以任意調整。

  2. 其他情況

- 避免使用select *
- count(1)或count(列) 代替 count(*)
- 建立表時儘量時 char 代替 varchar
- 表的欄位順序固定長度的欄位優先
- 組合索引代替多個單列索引(經常使用多個條件查詢時)
- 儘量使用短索引
- 使用連線(JOIN)來代替子查詢(Sub-Queries)
- 連表時注意條件型別需一致
- 索引雜湊值(重複少)不適合建索引,例:性別不適合