MySQL學習 - 索引

Balance-Tree & Balance+Tree 

爲何索引這麼快,一個好的索引能將檢索速度提高几個量級,這種效率離不開這個數據結構node


1.1 門路清

爲何須要"索引" ? 

咱們總得依據什麼才能去找你想查的東西,那麼咱們就依據 id=1去尋找一條記錄,怎麼找呢? 難道是"順序檢索" ?  sql

數據庫裏的東西如今大都大到分佈在不少個磁盤頁上。若是順序檢索基本就是噩夢。你知道"二分法" 比較快,那是由於數據已經被排過序了。 一樣的若是如今存在一種排過序的數據結構,使得咱們能快速去找出咱們想要的東西在哪兒,這樣必定很方便對吧。數據庫

因此歸根到底,如今最大的問題的就是"這條記錄到底存在哪"。 解決辦法是這樣的: 
  • 依據這個鍵好比ID,去產生一個序號,這樣就組成了 Key[序號]-Value[記錄的位置] 的形式
  • 這種K-V結構,會變成數據結構的一個節點。到時候咱們拿着ID,計算出序號,根據這個序號,在這種數據結構裏暢遊快速找到這個節點
  • 找到對應節點後查找出記錄的位置,去內存裏取,完成" select * from users where id = 1"


爲何叫它"索引" ? 

在上面的介紹中咱們已經看到了,實現快速找到內容的關鍵就是這個 "序號" 。這個所謂的序號就很是像"索引"這個名詞bash


爲何不用二分法查找?

理論上,二分法已是 O(logN), 很是快了。 實際上索引可能不少,多到你都沒辦法把索引所有加載到內存裏,因此這些索引基本上都在磁盤裏,依靠磁盤IO,分批次加載到內存裏。數據結構

既然提到磁盤IO,就應該知道磁盤IO效率很是低,低到實際上就,若是誰能減小磁盤IO次數,誰就會是最好的索引實現方案。優化

  • 二分法一次兩個節點
    • 整個樹很是的 "瘦高"  -> 
    • 咱們須要向下遍歷不少層 -> 
    • 咱們須要向下加載不少次磁盤IO -> 效率不高
  • 咱們想要把樹變的矮胖,減小層數



1.2 Balance-Tree


1.2.1 Balance-Tree中的節點爲何是這樣子 ?    查詢數據的過程ui

  • 一個節點中會包含不少個鍵值對, 以及不少個 "指針p" 咱們以 { 2 6 } 節點舉例看裏面有什麼
  • 節點內部是這樣安排的    {    [p1]     [序號=2]     [p2]     [序號=6]     [p3]      }
    • 假設一個如今 序號[6] 來了,正好能匹配上,前往6對應位置取回數據
    • 假設一個如今 序號[3] 來了,[3] 不匹配任何,可是位於2~6之間,前往p2繼續尋找
    • 假設一個如今 序號[1] 來了,[1] 不匹配任何,且小於2,前往p1繼續尋找
  • 節點內的全部元素都已經排過序了,所以拿着[序號]來節點裏查找的時候效率也比較高

剛剛,咱們就走完了一個節點,咱們完成了一輪查找。每一輪開始以前咱們會先執行磁盤IO,把下一個節點對應內容從磁盤加載到內存裏,而後再在組內查找,找獲得就退出,找不到繼續spa

根據上述結論,咱們也能發現一個重要結論: 既然咱們不能一次性把全部索引都加載到內存裏,既然咱們要分批次作磁盤IO。那樹的高度其實就是咱們IO的次數,那麼矮樹就會是最快的方案3d

func balance_tree_search (node *Node , key int) (*Data,error) {

    if node == nil {
        return nil,fmt.Errorf("最終也沒能找到對應序號")
    }

    for _, pair := range node {

        if (pair.Key == key){
            return pair.Data,nil
        }

        if (pair.Key > key) {
            // 考慮到節點內鍵值對是已經排序,從小到大的
            // 那既然上個鍵值對不知足,這個鍵值對又過於大
            // 那說明沒有符合的,前往下一個節點
            return balance_tree_search( pair.NextNode, key )
        }
    }

    // 比節點內全部鍵值對都大,直接前往下一個
    return balance_tree_search( pair.NextNode, key )
}  
複製代碼


1.2.2 Balance-Tree的插入過程指針



  • 如今是這樣,由於BT不能容許一個節點裏的有過多的鍵值對,所以當 [序號4] 過來的時候
  • 實際節點 { 3 5 } 不能容納 [序號4] 
  • 若是不能容納,那麼咱們會一路向上找到能容納的下的節點,因而咱們找到根節點
  • 相對應的,根節點變成兩個,多出一個位置用於存放指針,相對應的樹結構作出調整
  •  {   [p1]  [序號=4]  [p2]   }                   ->                 {   [p1] [序號=4] [p2] [序號=9] [p3]   }

自調整的過程雖然很漫長,看起來也很麻煩,可是這個剛好是知足了BT的自調整性質


1.2.3 Balance-Tree的刪除過程



  • 假設咱們如今想要刪除這個紅色的節點,可是刪除後 [序號12] 左邊會缺乏一個位置
  • 因而咱們如今須要調整一下,旋轉一下,成爲知足要求的Balance-Tree


1.2.4 Balance-Tree的定義

  • 依據自旋轉過程,保證樹都是同樣高的,m (也叫秩m)
  • 每一個節點上元素的數量  [m/2 ~ m]
  • 節點上元素須要排序


1.3 Balance+Tree


  • 在B+T中,中間節點並不會存儲任何跟"數據在那兒"相關的信息,只會作數據索引,指引你前往葉子節點。 
    • 不管怎樣你都要遍歷一下葉子節點,比BT更加穩定
    • 不須要前往中間節點,遍歷所有數據的時候比BT快
  • B+T全部節點都會按順序保存好數據,而且全部葉子節點都是串在一塊兒的,這樣當數據來的時候咱們只要按照粉色的路徑一路尋找就行了



關於索引,你須要知道


2.1 多個列,組合索引

  • 在你 create table 的時候假設你定義出 A+B+C 成爲你的組合索引,你須要知道這三列如今開始是不等價的,實際上這裏面只有第一列 A 重量級最高
    • 在上面咱們提到了[序號],那麼 A+B+C 如今就是這條數據的序號,在進行索引比較的時候會 先比較A,A相同了比較B,B相同了再去比較C
      • 多說一句,咱們不喜歡使用字符串做爲索引的緣由,是由於"字符串比較" 會比 "整數比較" 開銷更大,那種很長的字符串比較就更是麻煩了
    • A重量級最高:  若是你想真的從索引中受益,那麼你的WHERE篩選條件中必定要帶上這個重量級最高的A,不然索引沒有真的發揮做用,舉個例子,你能夠這麼使用索引
      • WHERE A=1                            (首列精確匹配)
      • WHERE A IS LIKE 1                  (首列的近似匹配)
      • WHERE A=1 AND B IS LIKE 2  (首列精確,二列近似)
    • 若是你嘗試在篩選條件裏不帶上A,那麼本次查找索引就根本沒有發揮做用
  • 咱們都知道 BT&B+T 能擁有一個很良好的"排序性質",咱們既然能按照排序的方式快速找到一個鍵值對,那麼在BT&B+T爲基礎的索引上
    • 完成 ORDER BY 很快
    • 完成範圍查找很快 ->  B IS IN ( 20,100 )


2.2 哈希索引

假設咱們定義出A+B+C做爲索引列,哈希索引就是針對每一條記錄計算出hash(A,B,C) 對應的值是這條記錄存儲的位置,哈希索引很是快,可是也有自身對應的一些弊端

  • 由於已經沒有排序結構了,所以ORDER BY 功能已經沒有那麼快了
  • 哈希功能須要全部索引列參與,你不能在只有B&C的狀況下去期望使用上哈希索引

2.2.1 哈希衝突

假設如今兩條記錄能哈希出同一個值,這種時候:

// 若是隻依賴hash 則返回兩條記錄
SELECT * FROM users WHERE hash(name) = 1;
>> liangxiaohan 23 M
   zhangxiaoming 24 F

// 最好的辦法是不只使用hash同時也指定索引列自身的值
// hash衝突下,造成鏈表,存儲引擎遍歷鏈表全部行
SELECT * FROM users WHERE hash(name) = 1 and name = "liangxiaohan"
>> liangxiaohan 23 M
複製代碼

2.2.2 自創索引

InnoDB支持哈希,但他的支持是指,它會自優化你的B樹索引成爲"某種程度上的"哈希索引。針對這一點,你能夠本身實現一個簡單的哈希索引

// 更新表,新建一列用於存放哈希值
ALTER TABLE ADD COLUMN name_crc VARCHAR(20)

// 關於哈希值,你可使用 TRIGGER 實現自動插入
// 你只負責插入name就好了,關於crc32哈希值它每次會本身計算
CREATE TRIGGER crc_create BEFORE INSERT ON users 
FOR EACH ROW SET NEW.name_crc = crc32(NEW.name)複製代碼


2.3 聚簇索引

2.3.1 關於聚簇索引,你須要知道

  • 聚簇索引 並非一種新型的索引,它所表明的只是一種數據存儲方式
  • 在聚簇索引,至關於一種變形B+T
    • 它的中間節點一樣不負責存儲任何數據
    • 它的葉子節點會存下這條記錄的全部內容實體,而不是一個指針
    • 葉子節點首位相接
  • 一個表只能有一個聚簇索引:按照聚簇索引的要求,數據會被存在一個指定的位置。可是數據不能既在這裏又在哪裏,因此只能有一個聚簇索引
  • 它之因此被叫作「聚簇」 : 是由於按照索引的要求,數據全都被存儲在了指定位置,而且索引上相鄰的位置,他們也存儲的很近

2.3.2 聚簇索引的優勢 & 缺點

  • 優勢
    • "聚簇"可能能夠發揮出很大的威力: 假設咱們令用戶ID成爲聚簇索引,那麼這個用戶可能全部相關的信息全存在一塊兒,咱們有可能存在一次IO讀取全部郵件
    • 訪問更快:  聚簇索引將索引+數據全都保存在同一個BT中,讀數據一般更快
  • 缺點
    • 假設一個應用,像是讀郵件這種應用,本質上是IO密集應用,若是這種時候咱們能好好利用聚簇索引,效果卓羣。可是若是數據全都存在內存中,並不涉及磁盤IO那就沒有多少優點了
    • 最大的問題:  執行插入的速度 。  由於在聚簇索引中,全部的數據已是直接存在B+T中而且排序了,那麼問題就來了
      • 若是我按照聚簇索引順序插入 -> 速度很快
      • 若是我隨機插入 -> 涉及數據的複製移動等,速度感人
    • 仍是關於聚簇的隨機插入: 頁分裂。 假設如今一個內存頁已經填滿了數據,可是如今有一個數據試圖在中間插入,存儲引擎會將這一頁分裂成兩頁,將第一頁的一部分複製到第二頁去
      • 效率極低
      • 產生內存碎片
      • 由於內存不連續致使掃描全表變慢
    • 若是真的要隨機插入,記得在插入完成之後使用 OPTIOMIZE TABLE 從新整理內存


上手試試? 

1. 一個查詢主頁很是慢的例子

背景: 獲取主頁面, 查詢前10條記錄,耗時2~3秒, 被描述爲" 不可忍受"的時間

1.1 咱們目前使用了那些數據庫索引?

MySQL[user] > SHOW INDEX FROM jobs;

*************************** 1. row ***************************
        Table: jobs
   Non_unique: 0
     Key_name: PRIMARY     <主鍵索引>
 Seq_in_index: 1
  Column_name: id
    Collation: A
  Cardinality: 13701
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
*************************** 2. row ***************************
        Table: jobs
   Non_unique: 1
     Key_name: userId      <惟一索引>
 Seq_in_index: 1
  Column_name: user_id
    Collation: A           
  Cardinality: 105
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE複製代碼
  • 目前存在兩隻索引,一隻主鍵索引  一隻普通惟一索引,區別:
    • 主鍵本質上是彙集索引,惟一索引做爲一種冗餘數據結構由數據庫維護 更多
  • Collation = A 說明是升序儲存
  • Packed = NULL 說明鍵未被壓縮 <更多字段解釋>


1.2 目前語句的查詢狀況是怎樣的? 

MySQL [compile]> 

EXPLAIN SELECT COUNT(j.id) 
FROM 
    (SELECT * FROM jobs WHERE user_id = 123 AND deleted_at is NULL) j 
LEFT JOIN 
    (SELECT * FROM builds WHERE deleted_at is NULL) b 
ON 
    b.id = j.latest_build_id \G

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2
        Extra: NULL
*************************** 2. row *************************** 
           id: 1
  select_type: PRIMARY
        table: <derived3>
         type: ref
possible_keys: <auto_key0>
          key: <auto_key0>
      key_len: 4
          ref: j.latest_build_id
         rows: 1187
        Extra: NULL
*************************** 3. row ***************************
           id: 3
  select_type: DERIVED     -- 內嵌表
        table: builds      -- 表名
         type: ALL         -- 全表掃描,效率 ALL < index < range < ref < const
possible_keys: NULL        -- 可能用到的索引
          key: NULL        -- 實際用到的索引
      key_len: NULL
          ref: NULL
         rows: 118713      -- 預期掃描行數
        Extra: Using where -- 使用Where作過濾,效率 filesort < temp < where < index
*************************** 4. row ***************************
           id: 2
  select_type: DERIVED
        table: jobs
         type: ref
possible_keys: userId
          key: userId
      key_len: 5
          ref: const
         rows: 1
        Extra: Using where

MySQL [compile]> SELECT COUNT(j.id) FROM (SELECT * FROM jobs WHERE user_id = 4 AND deleted_at is NULL) j LEFT JOIN (SELECT * FROM builds WHERE deleted_at is NULL) b ON b.id = j.latest_build_id;

+-------------+
| COUNT(j.id) |
+-------------+
|        1280 |
+-------------+
1 row in set (0.93 sec)

複製代碼
  • 順序由下向上,也就是說咱們先生成了表4, 表3 ,隨後才用上了表2 表1
  • 關於每個字段的含義:   這裏
  • 光是在數據庫裏執行就消耗1280行,真是難以忍受的時間


1.3 查詢成本估計解讀

  • 產生表4 :  預計掃描 1 行     ->   假設產生 a 行
  • 產生表3 :  預期掃描 118713 行  ->  假設產生 b 行
  • LEFT-JOIN 3&4 :  預計掃描 a * b = 1187 行  ->  假設產生 c 行
  • SELECT COUNT(j.id) 預計掃描 2 行
  • 【預測】總共掃描行數  1 + 118713 + 1187 + 2
相關文章
相關標籤/搜索