mysql學習筆記三--關於索引
當一個表的數據量達到一定程度時,查詢速度會急速下降,這時候就需要適當地添加索引來加快查詢速度。雖然在日常工作中會經常接觸到索引,上周翼賽用戶登錄部分,當通過oauth_id查詢race_user_oauth_token表的數據記錄時,平均一次查詢需要3s以上,在用戶數量集中增長時,導致數據庫壓力急劇增長,當為oauth_id添加索引後,一切都恢復正常。雖然每天都能接觸到索引,但好像對其都是一知半解的,於是決定查看相關的書本、專欄和博客來深入學習一下mysql的InnoDB索引。
1、為什麽需要索引?
索引類似於書本的目錄,最終都是為了提高查詢效率而存在的。
2、常見的索引數據模型主要有三種:哈希表、有序數組以及樹。
(1)哈希表是一種鍵值對存儲方式,相信大家都比較熟悉了,把值放在數組裏,用一個哈希函數把key換算成一個確定的位置,然後把value放在數組的這個位置。但是這樣經常會引發哈希沖突,解決哈希沖突常用的方法是鏈表,當不同的值經過哈希函數處理後對應的key是同一個是,這些值就用指針連接起來形成鏈表。哈希表在等值查詢的場景中表現非常優秀,比如redis以及memcached等nosql存儲方式。但是哈希表並不適用於區間查詢,當需要區間查詢時,只能全表掃描了。這種存儲方式很適合數據庫的主鍵查詢,但當不是根據主鍵查詢時,我們需要另外存儲一遍key對應的所有記錄。例如,當前存儲的user記錄為uid對應user record,但當我們需要通過user name來獲取user record時,我們需要將username作為key再進行存儲,因此雖然查詢效率非常高,但是確實有點類似於拿空間換時間的做法,之前翼賽用戶登錄部分防止高並發對用戶數據進行redis緩存了就存儲了很多遍相同的用戶記錄,對應不同的key。
(2)有序數組相信大家都非常熟悉了,對於區間查詢和等值查詢性能都比較優秀,時間復雜度為O(log(N)),但有序數組也有個致命的缺點是更新操作非常耗時,當需要往中間插入一條新記錄時,需要把所有的記錄都向後挪動,此時成本非常高,因此有序數組比較適用於靜態存儲引擎。
(3)樹結構最經典的應該是二叉搜索樹了,每個節點的左兒子小於父節點,父節點又小於右兒子,這樣查詢起來效率非常高,當我們進行前序遍歷的時候,就相當於順序數組的二分查找了,因為時間復雜度為O(log(N)),每次更新也需要保持二叉樹的平衡,時間復雜度也是 O(log(N))。但是,對於使用樹作為數據庫存儲結構,我們通常不采用二叉樹,這是因為索引不僅要寫到內存中,還要寫到磁盤上。例如一棵一百萬個節點的二叉樹,樹高20,那麽我們就需要20個數據頁來存儲,此時最壞的遍歷查詢需要搜索這20個數據頁,這遠遠不能滿足我們高速查詢數據的要求。(思考:為什麽樹的每一層都要對應一個數據頁呢?將實際的物理存儲結構模擬成一棵樹,每個頁之間通過指針關聯起來)
3、關於InnoDB索引:
因此,為了在查詢過程盡可能少的訪問數據頁,我們就需要將二叉樹改為N叉樹,這裏的N取決於頁的大小。為什麽是這樣的呢?我們拿當下使用很普遍的InnoDB存儲引擎來說明:
我們都知道計算機在存儲數據的時候,有最小存儲單元,這就好比我們今天進行現金的流通最小單位是一毛。在計算機中磁盤存儲數據最小單元是扇區,一個扇區的大小是512字節,而文件系統(例如XFS/EXT4)他的最小單元是塊,一個塊的大小是4k,而對於我們的InnoDB存儲引擎也有自己的最小儲存單元——頁(Page),一個頁的大小是16K。可以通過如下語句查看,也可以通過參數自行設置:
mysql> show variables like ‘innodb_page_size‘; +------------------+-------+ | Variable_name | Value | +------------------+-------+ | innodb_page_size | 16384 | +------------------+-------+ 1 row in set (0.00 sec)
數據表中的數據都是存儲在頁中的,所以一個頁中能存儲多少行數據呢?假設一行數據的大小是1k,那麽一個頁可以存放16行這樣的數據。如果數據庫只按這樣的方式存儲,那麽如何查找數據就成為一個問題,因為我們不知道要查找的數據存在哪個頁中,也不可能把所有的頁遍歷一遍,那樣太慢了。
在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,所以數據都是存儲在 B +樹中的。每一個索引在InnoDB中都對應一棵B+樹。
如下例子,我們可以看到數據的InnoDB中具體的存儲形式:
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k))engine=InnoDB;
如上語句,我們建了一個表,id為主鍵,k上有索引。
由上圖我們可以看到,數據記錄按主鍵進行排序,分別存放在不同的頁中,除了存放數據的頁以外,還有存放鍵值+指針的頁,如圖中ID對應的第一行,即為一頁,該頁存放鍵值和指向數據頁的指針,這樣的頁由N個鍵值+指針組成。當然它也是排好序的。那麽要查找一條數據,應該怎麽查?如:
select * from T where id=300;
這裏id是主鍵,我們通過這棵B+樹來查找,首先找到根頁,每張表的根頁位置在表空間文件中是固定的,即圖中ID索引對應的B+樹的第一層,找到根頁後通過二分查找法,定位到id=300數據應該在B+樹根節點的右兒子對應的頁中,那麽進一步去該頁中查找,同樣通過二分查詢法即可找到id=300的記錄。
上面的例子我們討論的是主鍵索引查詢數據的情況,那麽非主鍵索引、即字段k上的索引對應的B+樹是如何存儲的呢?主鍵索引和非主鍵索引的區別在哪裏呢?
主鍵索引又稱為聚簇索引,存儲的是對應的完整的一行數據記錄,若一個表沒有設置主鍵,mysql會添加一個row_id作為主鍵,非主鍵索引只存儲對應行數據的主鍵,通過主鍵再去查詢對應的行記錄,此過程稱為回表。在不影響排序結果的情況下,在取出主鍵後,回表之前,會在對所有獲取到的主鍵排序,這就是mysql的Multi-Range Read (MRR)策略。當我們使用count、sum等統計函數或select id from T where k=3 等只查詢主鍵字段語句時,甚至不用進行回表操作便可以直接得到結果,也就是說k上的索引已經“覆蓋”了我們的查詢要求,稱為覆蓋索引,時間可大大縮減。也就是說,基於非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應盡可能地使用主鍵索引。
接下來又有一個新的問題?為什麽非主鍵索引不存儲對應的行數據,而只存儲對應的主鍵呢?為了在有限的空間內存儲盡可能多的數據,也即時間和空間代價相權衡的問題。
4、一個比較經典的問題是:InnoDB一棵B+樹可以存放多少行數據?
這裏我們先假設B+樹高為2,即存在一個根節點和若幹個葉子節點,那麽這棵B+樹的存放總記錄數為:根節點指針數*單個葉子節點記錄行數。
上文我們說過通常一頁的大小為16k,則單個葉子節點(頁)中的記錄數=16K/1K=16。(這裏假設一行記錄的數據大小為1k,實際上現在很多互聯網業務數據記錄大小通常就是1K左右)。那麽現在我們需要計算出非葉子節點能存放多少指針,其實這也很好算,我們假設主鍵ID為bigint類型,長度為8字節,而指針大小在InnoDB源碼中設置為6字節,這樣一共14字節,我們一個頁中能存放多少這樣的單元,其實就代表有多少指針,即16384/14=1170。那麽可以算出一棵高度為2的B+樹,能存放1170*16=18720條這樣的數據記錄。根據同樣的原理我們可以算出一個高度為3的B+樹可以存放:1170*1170*16=21902400條這樣的記錄。所以在InnoDB中B+樹高度一般為1-3層,它就能滿足千萬級的數據存儲。在查找數據時一次頁的查找代表一次IO,所以通過主鍵索引查詢通常只需要1-3次IO操作即可查找到數據。
5、既然索引能大大的提高查詢速度,那麽我們是不是每個字段都加上索引呢?
答案肯定是no了,別忘了我們對數據庫的操縱包括增刪改查,當我們更新數據庫、插入一行數據、或者刪除主鍵索引時都需要維護相應的索引,如果索引數目過多,可能會需要非常高的更新成本。當一個數據頁滿了,按照B+樹算法,需新增加一個數據頁,叫做頁分裂,會導致性能下降。空間利用率降低大概50%。當相鄰的兩個數據頁利用率很低的時候會做數據頁合並,合並的過程是分裂過程的逆過程。如果刪除,新建主鍵索引,會同時去修改普通索引對應的主鍵索引,性能消耗比較大。
6、關於聯合索引:
聯合索引也對應為一棵B+樹,如下圖所示:
關於聯合索引,大部分都和普通索引類似,關鍵在於聯合索引的最左前綴原則。當我們需要所有名字是“張三”的人時,可以快速定位到 ID4,然後向後遍歷便可以得到所有結果。當我們要查詢所有名字第一個字是“張”的人,SQL語句如“select * from user where name like ‘張%‘”,這時,你也能夠用上這個索引,查找到第一個符合條件的記錄是 ID3,然後向後遍歷,直到不滿足條件為止。
可以看到,不只是索引的全部定義,只要滿足最左前綴,就可以利用索引來加速檢索。這個最左前綴可以是聯合索引的最左 N 個字段,也可以是字符串索引的最左 M 個字符。需要註意的時候聯合索引的存儲順序就是定義索引時的字段順序,我們在建立索引時,需要考慮這個最左前綴原則,充分發揮聯合索引的效用,以及盡可能少地建立功能一樣的單個索引。也即當我們有了(username,age)這個聯合索引時,就不需要再單獨建立username這個索引了。還有就是需要基於空間上的考慮,username字段所需的空間大於age所需空間,因此我們可以用聯合索引(username,age)聯合索引和age單獨索引,而非(username,age)聯合索引和username單獨索引。
最後在mysql5.6之後的版本,新增了聯合索引上的索引下推功能,即當我們查詢姓“張”且年齡為10歲的人時,sql語句為“select * from user where username like ‘張%‘ and age=10”,當我們在聯合索引的B+樹中查詢出張姓用戶時,此時可以不用先去回表查詢用戶記錄後再篩選出年齡為10的用戶,而是直接在當前的B+樹中過濾掉年齡不為10的用戶,這就大大地提高了查詢效率。
7、學習資料:
《MySQL技術內幕.SQL編程》、專欄MySQL實戰45講--深入淺出MySQL索引(上)、(下)。
mysql學習筆記三--關於索引