1. 程式人生 > 實用技巧 >深入淺出mysql優化--一篇部落格讓你精通mysql優化策略--上

深入淺出mysql優化--一篇部落格讓你精通mysql優化策略--上

一篇部落格和大家一起學習mysql優化的通用策略。
內容花了好些時間來整理書寫,如果覺得有用,還請點個贊,還有就是發現部落格園的MD文字格式和在其他軟體的不太同,格式調的也不好,還請將就一下
接下來一起來學習一下mysql優化的內容吧

1. 一條查詢sql的執行過程

select * from T where ID=10;  的執行過程詳解
  • MySQL 的邏輯架構

    大體來說,MySQL可以分為 Server層 和 儲存引擎層 兩部分

    Server層:
    包括聯結器、查詢快取、分析器、優化器、執行器等,涵蓋 MySQL 的大多數核心服務功能,
    以及所有的內建函式(如日期、時間、數學和加密函式等),所有跨儲存引擎的功能都在這一層實現,
    比如儲存過程、觸發器、檢視等

    儲存引擎層:
    負責資料的儲存和提取。
    其架構模式是外掛式的,支援 InnoDB、MyISAM、Memory 等多個儲存引擎。
    現在最常用的儲存引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成為了預設儲存引擎

    執行create table建表的時候,如果不指定引擎型別,預設使用的就是InnoDB。
    不過,也可以通過指定儲存引擎的型別來選擇別的引擎,
    比如在 create table語句中使用 engine=memory, 來指定使用記憶體引擎建立表。
    不同儲存引擎的表資料存取方式不同,支援的功能也不同
    從圖中不難看出,不同的儲存引擎共用一個Server層,也就是從聯結器到執行器的部分。
    
  • 第一步:聯結器

    第一步,先連線到這個資料庫上,這時候接待的就是聯結器。
    聯結器負責跟客戶端建立連線、獲取許可權、維持和管理連線。
    連線命令一般是這麼寫的: mysql -h$ip -P$port -u$user -p

    輸完命令之後,需要在互動對話裡面輸入密碼。
    雖然密碼也可以直接跟在 -p 後面寫在命令列中,但這樣可能會導致密碼洩露。
    如果連的是生產伺服器,強烈建議不要這麼做

    如果連線命令中的 mysql 是客戶端工具,用來跟服務端建立連線。
    在完成經典的 TCP 握手後,聯結器就要開始認證身份,這個時候用的就是輸入的使用者名稱和密碼

     1. 如果使用者名稱或密碼不對,就會收到一個"Access denied for user"的錯誤,然後客戶端程式結束執行
     2. 如果使用者名稱密碼認證通過,聯結器會到許可權表裡面查出使用者擁有的許可權。
        之後,這個連線裡面的許可權判斷邏輯,都將依賴於此時讀到的許可權
     
     這就意味著,一個使用者成功建立連線後,即使用管理員賬號對這個使用者的許可權做了修改,
     也不會影響已經存在連線的許可權,修改完成後,只有再新建的連線才會使用新的許可權設定
    

    連線完成後,如果沒有後續的動作,這個連線就處於空閒狀態,可以在 show processlist 命令中看到它,
    以下圖其中的Command列顯示為“Sleep”的這一行,就表示現在系統裡面有一個空閒連線

 客戶端如果太長時間沒動靜,聯結器就會自動將它斷開。
 這個時間是由引數 wait_timeout控制的,預設值是 8 小時
 
 如果在連線被斷開之後,客戶端再次傳送請求的話,就會收到一個錯誤提醒:Lost connection to MySQL server during query。
 這時候如果要繼續,就需要重連,然後再執行請求了
  • 長連線和短連線

    長連線: 是指連線成功後,如果客戶端持續有請求,則一直使用同一個連線。
    短連線: 是指每次執行完很少的幾次查詢就斷開連線,下次查詢再重新建立一個。

    建立連線的過程通常是比較複雜的,所以建議在使用中要儘量減少建立連線的動作,也就是儘量使用長連線

    但是全部使用長連線後,可能會發現,有些時候 MySQL 佔用記憶體漲得特別快,
    這是因為 MySQL 在執行過程中臨時使用的記憶體是管理在連線物件裡面的。這些資源會在連線斷開的時候才釋放。
    所以如果長連線累積下來,可能導致記憶體佔用太大,被系統強行殺掉(OOM),從現象看就是 MySQL 異常重啟了

    那麼怎麼解決這個問題呢?可以考慮以下兩種方案

     1. 定期斷開長連線。使用一段時間,或者程式裡面判斷執行過一個佔用記憶體的大查詢後,斷開連線,之後要查詢再重連
     2. 如果你用的是 MySQL 5.7 或更新版本,可以在每次執行一個比較大的操作後,通過執行 mysql_reset_connection 
        來重新初始化連線資源。這個過程不需要重連和重新做許可權驗證,但是會將連線恢復到剛剛建立完時的狀態。
    
  • 第二步:查詢快取

    連線建立完成後,就可以執行 select 語句了。執行邏輯就會來到第二步:查詢快取

    MySQL 拿到一個查詢請求後,會先到 查詢快取 看看之前是不是執行過這條語句。
    之前執行過的語句及其結果可能會以 key-value 對的形式被直接快取在記憶體中。key 是查詢的語句,value 是查詢的結果。
    如果當前的查詢能夠直接在這個快取中找到 key,那麼這個value 就會被直接返回給客戶端

    如果語句不在查詢快取中,就會繼續後面的執行階段。執行完成後,執行結果會被存入查詢快取中。
    可以看到,如果查詢命中快取,MySQL不需要執行後面的複雜操作,就可以直接返回結果,這個效率會很高。

    但是大多數情況下建議不要使用查詢快取,為什麼呢?

     因為查詢快取往往弊大於利
     查詢快取的失效非常頻繁,只要有對一個表的更新,這個表上所有的查詢快取都會被清空。
     因此很可能費勁地把結果存起來,還沒使用呢,就被一個更新全清空了。
     對於更新壓力大的資料庫來說,查詢快取的命中率會非常低。除非業務就是有一張靜態表,很長時間才會更新一次。
     比如,一個系統配置表,那這張表上的查詢才適合使用查詢快取
    

    好在 MySQL 也提供了這種“按需使用”的方式。
    可以將引數 query_cache_type 設定成 DEMAND,這樣對於預設的 SQL 語句都不使用查詢快取。
    而對於確定要使用查詢快取的語句,可以用 SQL_CACHE 顯式指定,像下面這個語句一樣

     select SQL_CACHE * from T where ID=10
     需要注意的是,MySQL 8.0 版本直接將查詢快取的整塊功能刪掉了,也就是說 8.0 開始徹底沒有這個功能了
    
  • 第三步:分析器

    如果沒有命中查詢快取,就要開始真正執行語句了。MySQL 需要知道要做什麼,因此需要對 SQL 語句做解析。

    分析器先會做“詞法分析”。
    輸入的是由多個字串和空格組成的一條 SQL 語句,MySQL需要識別出裡面的字串分別是什麼,代表什麼。

    MySQL 從輸入的"select"這個關鍵字識別出來,這是一個查詢語句。它也要把字串“T”識別成“表名 T”,把字串“ID”識別成“列 ID”

    做完了這些識別以後,就要做“語法分析”。
    根據詞法分析的結果,語法分析器會根據語法規則,判斷輸入的這個 SQL 語句是否滿足 MySQL 語法

    如果語句不對,就會收到“You have an error in your SQL syntax”的錯誤提醒,比如下面這個語句 select 少打了開頭的字母“s”

     elect * from t where ID=1
     You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for 
     the right syntax to use near 'elect * from t where ID=1' at line 1
     一般語法錯誤會提示第一個出現錯誤的位置,所以你要關注的是緊接“use near”的內容
    
  • 第四步:優化器

    經過了分析器,MySQL 就知道要做什麼了。在開始執行之前,還要先經過優化器的處理

    優化器是在表裡面有多個索引的時候,決定使用哪個索引,或者在一個語句有多表關聯(join)的時候,決定各個表的連線順序。
    比如執行下面這樣的語句,這個語句是執行兩表的 join

     select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
     
     既可以先從表 t1 裡面取出 c=10 的記錄的 ID 值,再根據 ID 值關聯到表 t2,再判斷 t2裡面 d 的值是否等於 20
     也可以先從表 t2 裡面取出 d=20 的記錄的 ID 值,再根據 ID 值關聯到 t1,再判斷 t1 裡面 c 的值是否等於 10
    

    這兩種執行方法的邏輯結果是一樣的,但是執行的效率會有不同,而優化器的作用就是決定選擇使用哪一個方案

    優化器階段完成後,這個語句的執行方案就確定下來了,然後進入執行器階段。

  • 第五步:執行器

    MySQL 通過分析器知道了要做什麼,通過優化器知道了該怎麼做,於是就進入了執行器階段,開始執行語句

    開始執行的時候,要先判斷一下當前使用者對這個表 T 有沒有執行查詢的許可權,如果沒有,就會返回沒有許可權的錯誤,
    如下所示 (在工程實現上,如果命中查詢快取,會在查詢快取返回結果的時候,做許可權驗證。查詢也會在優化器之前呼叫 precheck 驗證許可權)

     select * from T where ID=10;
     ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
    

    如果有許可權,就開啟表繼續執行。開啟表的時候,執行器就會根據表的引擎定義,去使用這個引擎提供的介面

    比如這個例子中的表T中,ID 欄位沒有索引,那麼執行器的執行流程是這樣的:

     1. 呼叫 InnoDB 引擎介面取這個表的第一行,判斷 ID 值是不是 10,如果不是則跳過,如果是則將這行存在結果集中;
     2. 呼叫引擎介面取“下一行”,重複相同的判斷邏輯,直到取到這個表的最後一行。
     3. 執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作為結果集返回給客戶端
    

    至此,這個語句就執行完成了

    對於有索引的表,執行的邏輯也差不多。
    第一次呼叫的是“取滿足條件的第一行”這個介面,之後迴圈取“滿足條件的下一行”這個介面,這些介面都是引擎中已經定義好的

    在資料庫的慢查詢日誌中可以看到一個 rows_examined 的欄位,表示這個語句執行過程中掃描了多少行。
    這個值就是在執行器每次呼叫引擎獲取資料行的時候累加的

    在有些場景下,執行器呼叫一次,在引擎內部則掃描了多行,因此引擎掃描行數跟rows_examined 並不是完全相同的。

  • 問題

    如果表 T 中沒有欄位 k,而執行了這個語句 select * from T where k=1,
    那肯定是會報“不存在這個列”的錯誤: “Unknown column ‘k’ in ‘where clause’”。
    那麼這個錯誤是在我們上面提到的哪個階段報出來的呢?

     分析器
    

2. 一條更新sql的執行過程

create table T(ID int primary key, c int);
update T set c=c+1 where ID=2;

在第一部分說過,在一個表上有更新的時候,跟這個表有關的查詢快取會失效,
所以這條語句就會把表 T 上所有快取結果都清空。這也就是我們一般不建議使用查詢快取的原因。

接下來,分析器會通過詞法和語法解析知道這是一條更新語句。
優化器決定要使用 ID 這個索引。
然後,執行器負責具體執行,找到這一行,然後更新。

與查詢流程不一樣的是,更新流程還涉及兩個重要的日誌模組:

    redo log(重做日誌)和 binlog(歸檔日誌)。
  • 重要的日誌模組:redo log

    以《孔乙己》這篇文章作為例子,
    酒店掌櫃有一個粉板,專門用來記錄客人的賒賬記錄。
    如果賒賬的人不多,那麼他可以把顧客名和賬目寫在板上。
    但如果賒賬的人多了,粉板總會有記不下的時候,這個時候掌櫃一定還有一個專門記錄賒賬的賬本
    
    如果有人要賒賬或者還賬的話,掌櫃一般有兩種做法:
    
         1. 直接把賬本翻出來,把這次賒的賬加上去或者扣除
         2. 先在粉板上記下這次的賬,等打烊以後再把賬本翻出來核算
    
    在生意紅火櫃檯很忙時,掌櫃一定會選擇後者,因為前者操作實在是太麻煩了。
    首先,你得找到這個人的賒賬總額那條記錄。可能找要一段實踐,找到後再拿出算盤計算,最後再將結果寫回到賬本上
    
    這整個過程想想都麻煩。相比之下,還是先在粉板上記一下方便。
    想想,如果掌櫃沒有粉板的幫助,每次記賬都得翻賬本,效率是不是低得讓人難以忍受?
    
    
    同樣,在 MySQL 裡也有這個問題,如果每一次的更新操作都需要寫進磁碟,然後磁碟也要找到對應的那條記錄,然後再更新,
    整個過程 IO 成本、查詢成本都很高。為了解決這個問題,MySQL 的設計者就用了類似酒店掌櫃粉板的思路來提升更新效率。
    
    而粉板和賬本配合的整個過程,其實就是 MySQL 裡經常說到的 WAL 技術,
    WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是:先寫日誌,再寫磁碟,也就是先寫粉板,等不忙的時候再寫賬本
    
    具體來說,當有一條記錄需要更新的時候,InnoDB 引擎就會先把記錄寫到 redo log裡面,並更新記憶體,
    這個時候更新就算完成了。
    同時,InnoDB 引擎會在適當的時候,將這個操作記錄更新到磁盤裡面,而這個更新往往是在系統比較空閒的時候做, 
    
    如果今天賒賬的不多,掌櫃可以等打烊後再整理。
    但如果某天賒賬的特別多,粉板寫滿了,
    又怎麼辦呢?這個時候掌櫃只好放下手中的活兒,把粉板中的一部分賒賬記錄更新到賬本中,
    然後把這些記錄從粉板上擦掉,為記新賬騰出空間         
    
    與此類似,InnoDB 的 redo log 是固定大小的,比如可以配置為一組 4個檔案,
    每個檔案的大小是 1GB,那麼這塊“粉板”總共就可以記錄 4GB 的操作。
    從頭開始寫,寫到末尾就又回到開頭迴圈寫,
    如下面這個圖所示
    

    write pos是當前記錄的位置,一邊寫一邊後移,寫到第 3 號檔案末尾後就回到 0 號檔案開頭。
    checkpoint 是當前要擦除的位置,也是往後推移並且迴圈的,擦除記錄前要把記錄更新到資料檔案
    
    
    write pos 和 checkpoint 之間的是“粉板”上還空著的部分,可以用來記錄新的操作。
    如果 write pos 追上 checkpoint,表示“粉板”滿了,這時候不能再執行新的更新,
    得停下來先擦掉一些記錄,把 checkpoint 推進一下
    
    有了 redo log,InnoDB 就可以保證即使資料庫發生異常重啟,
    之前提交的記錄都不會丟失,這個能力稱為crash-safe
    
    要理解 crash-safe 這個概念,可以想想前面賒賬記錄的例子。
    只要賒賬記錄記在了粉板上或寫在了賬本上,之後即使掌櫃忘記了,比如突然停業幾天,
    恢復生意後依然可以通過賬本和粉板上的資料明確賒賬賬目
    
  • 重要的日誌模組:binlog

    redo log 是 InnoDB 引擎特有的日誌,而 Server 層也有自己的日誌,稱為 binlog

    最開始 MySQL 裡並沒有 InnoDB 引擎。
    MySQL 自帶的引擎是 MyISAM,但是MyISAM 沒有 crash-safe 的能力,binlog 日誌只能用於歸檔。
    而 InnoDB 是另一個公司以外掛形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,
    所以 InnoDB使用另外一套日誌系統——也就是 redo log 來實現 crash-safe 能力

    兩種日誌有以下三點不同

     1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
     2. redo log 是物理日誌,記錄的是“在某個資料頁上做了什麼修改”;
        binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給 ID=2 這一行的 c 欄位加 1 ”。
     3. redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。
        “追加寫”是指 binlog 檔案寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌
    

    update 語句時的內部流程

     1. 執行器先找引擎取 ID=2 這一行。ID 是主鍵,引擎直接用樹搜尋找到這一行。如果
        ID=2 這一行所在的資料頁本來就在記憶體中,就直接返回給執行器;否則,需要先從磁碟
        讀入記憶體,然後再返回。
     2. 執行器拿到引擎給的行資料,把這個值加上 1,比如原來是 N,現在就是 N+1,得到新
        的一行資料,再呼叫引擎介面寫入這行新資料。
     3. 引擎將這行新資料更新到記憶體中,同時將這個更新操作記錄到 redo log 裡面,此時
        redo log 處於 prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。
     4. 執行器生成這個操作的 binlog,並把 binlog 寫入磁碟。
     5. 執行器呼叫引擎的提交事務介面,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成
    

    update 語句的執行流程圖,圖中淺色框表示是在 InnoDB 內部執行的,深色框表示是在執行器中執行的

 將 redo log 的寫入拆成了兩個步驟:
 
    prepare 和 commit,這就是"兩階段提交"。
  • 兩階段提交

     存在兩階段提交為了讓兩份日誌之間的邏輯一致,怎樣讓資料庫恢復到半個月內任意一秒的狀態?
     
         binlog 會記錄所有的邏輯操作,並且是採用“追加寫”的形式。如果你的 DBA 承諾說半個月內可以恢復,
         那麼備份系統中一定會儲存最近半個月的所有binlog,同時系統會定期做整庫備份。
         這裡的“定期”取決於系統的重要性,可以是一天一備,也可以是一週一備。
         當需要恢復到指定的某一秒時,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回資料,那你可以這麼做:
         
             首先,找到最近的一次全量備份,如果你運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫;
             然後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到中午誤刪表之前的那個時刻。
         
         這樣你的臨時庫就跟誤刪之前的線上庫一樣了,然後你可以把表資料從臨時庫取出來,按需要恢復到線上庫去。
    

    為什麼日誌需要“兩階段提交”

     由於 redo log 和 binlog 是兩個獨立的邏輯,如果不用兩階段提交,要麼就是先寫完 redolog 再寫 binlog,
     或者採用反過來的順序。看看這兩種方式會有什麼問題
     
     仍然用前面的 update 語句來做例子。
     假設當前 ID=2 的行,欄位 c 的值是 0,再假設執行update 語句過程中在寫完第一個日誌後,
     第二個日誌還沒有寫完期間發生了 crash,會出現什麼情況呢?
     
       1.先寫 redo log 後寫 binlog。
         假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 程序異常重啟。
         由於前面說過的,redo log 寫完之後,系統即使崩潰,仍然能夠把資料恢復回來,所以恢復後這一行 c 的值是 1。
         但是由於 binlog 沒寫完就 crash 了,這時候 binlog 裡面就沒有記錄這個語句。
         因此,之後備份日誌的時候,存起來的 binlog 裡面就沒有這條語句。
         然後會發現,如果需要用這個 binlog 來恢復臨時庫的話,由於這個語句的 binlog 丟失,
         這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不同。
         
       2.先寫 binlog 後寫 redo log。
         如果在 binlog 寫完之後 crash,由於 redo log 還沒寫,
         崩潰恢復以後這個事務無效,所以這一行 c 的值是 0。但是 binlog 裡面已經記錄了“把c 從 0 改成 1”這個日誌。
         所以,在之後用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同
         
     可以看到,如果不使用“兩階段提交”,那麼資料庫的狀態就有可能和用它的日誌恢復出來的庫的狀態不一致。
     這個概率是不是很低,平時也沒有什麼動不動就需要恢復臨時庫的場景呀?
     其實不是的,不只是誤操作後需要用這個過程來恢復資料。當需要擴容的時候,也就是需要再多搭建一些備庫來增加系統的讀能力的時候,
     現在常見的做法也是用全量備份加上應用binlog 來實現的,這個“不一致”就會導致你的線上出現主從資料庫不一致的情況。
     簡單說,redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。
    
  • tip

     redo log 用於保證 crash-safe 能力。
     innodb_flush_log_at_trx_commit 這個引數設定成1 的時候,表示每次事務的 redo log 都直接持久化到磁碟。
     這個引數建議設定成 1,這樣可以保證 MySQL 異常重啟之後資料不丟失。
     sync_binlog 這個引數設定成 1 的時候,表示每次事務的 binlog 都持久化到磁碟。
     這個引數也建議你設定成 1,這樣可以保證 MySQL 異常重啟之後 binlog 不丟失。
     
     在什麼場景下,一天一備會比一週一備更有優勢呢?或者說,它影響了這個資料庫系統的哪個指標?
     
         一天一備跟一週一備的對比。
         好處是“最長恢復時間”更短。
         在一天一備的模式裡,最壞情況下需要應用一天的 binlog。
         比如,每天 0 點做一次全量備份,而要恢復出一個到昨天晚上 23 點的備份。
         一週一備最壞情況就要應用一週的 binlog 了。
         系統的對應指標就是恢復目標時間
         頻繁全量備份需要消耗更多儲存空間,所以這個 RTO 是成本換來的,需要根據業務重要性來評估
    

3. 深入淺出mysql索引

  • 使用hash索引儲存

如果要維護一個身份證資訊和姓名的表,需要根據身份證號查詢對應的名字,這時 對應的雜湊索引的示意圖如下所示

圖中,User2 和 User3 根據身份證號算出來的值都是 n,後面還跟了一個連結串列。
如果這時候要查 card-2 對應的名字是什麼,處理步驟就是:
    首先,將 card-2 通過雜湊函式算出n,然後,按順序遍歷,找到 User2。
需要注意的是,圖中四個 card-n 的值並不是遞增的,這樣做的好處是增加新的 User 時速度會很快,只需要往後追加。
但缺點是,因為不是有序的,所以雜湊索引做 區間查詢 的速度是很慢的。
如果現在要找身份證號在 [card_X, card_Y] 這個區間的所有使用者,就必須全部掃描一遍了。
所以,雜湊表這種結構適用於只有等值查詢的場景,比如 Memcached 及其他一些 NoSQL 引擎,這一點上一邊索引型別介紹中已經說得很清楚了
  • 有序陣列

有序陣列 在等值查詢和範圍查詢場景中的效能就都非常優秀,以下是其索示意圖

假設身份證號沒有重複,這個陣列就是按照身份證號遞增的順序儲存的。
這時候如果要查 card_n2 對應的名字,用二分法就可以快速得到,這個時間複雜度是 O(log(N))。
同時很顯然,這個索引結構支援範圍查詢。你要查身份證號在 [card_X, card_Y] 區間的user,
    可以先用二分法找到 card_X(如果不存在card_X,就找到大於card_X 的第一個user),然後向右遍歷,直到查到第一個大於card_Y 的身份證號,退出迴圈。
如果僅僅看查詢效率,有序陣列就是最好的資料結構了。
但是,在需要更新資料的時候卻不好,你往中間插入一個記錄就必須得挪動後面所有的記錄,成本太高。
所以,有序陣列索引只適用於靜態儲存引擎,比如你要儲存的是2020年某個城市的所有人口資訊,這類不會再修改的資料
  • 二叉搜尋樹示意圖

二叉搜尋樹的特點是:

每個節點的左兒子小於父節點,父節點又小於右兒子。這樣如果你要查card_n2 的話,按照圖中的搜尋順序就是按照 UserA -> UserC -> UserF -> User2 這個路徑得到。這個時間複雜度是 O(log(N))。
當然為了維持 O(log(N)) 的查詢複雜度,你就需要保持這棵樹是平衡二叉樹。為了做這個 保證,更新的時間複雜度也是 O(log(N))。
樹可以有二叉,也可以有多叉。多叉樹就是每個節點有多個兒子,兒子之間的大小保證從左 到右遞增。
二叉樹是搜尋效率最高的,但是實際上大多數的資料庫儲存卻並不使用二叉樹。 其原因是,索引不止存在記憶體中,還要寫到磁碟上。
你可以想象一下一棵 100 萬節點的平衡二叉樹,樹高20。一次查詢可能需要訪問 20 個數據塊。
在機械硬碟時代,從磁碟隨機讀一個數據塊需要 10 ms 左右的定址時間。也就是說,對於一個100萬行的表,如果使用二叉樹來儲存,單獨訪問一個行可能需要 20 個10 ms 的時間

為了讓一個查詢儘量少地讀磁碟,就必須讓查詢過程訪問儘量少的資料塊。
那麼,我們就不應該使用二叉樹,而是要使用“N 叉”樹。這裡,“N 叉”樹中的“N”取決於資料塊的大小。
以 InnoDB 的一個整數字段索引為例,這個N差不多是 1200。這棵樹高是 4 的時候,就可以存 1200 的 3 次方個值,這已經 17 億了。
考慮到樹根的資料塊總是在記憶體中的,一個 10 億行的表上一個整數字段的索引,查詢一個值最多隻需要訪問3次磁碟。
其實,樹的第二層也有很大概率在記憶體中,那麼訪問磁碟的平均次數就更少了。
N叉樹由於在讀寫上的效能優點,以及適配磁碟的訪問模式,已經被廣泛應用在資料庫引擎中了。

在 MySQL 中,索引是在儲存引擎層實現的,所以並沒有統一的索引標準,即不同儲存引 擎的索引的工作方式並不一樣。而即使多個儲存引擎支援同一種類型的索引,其底層的實現 也可能不同。由於 InnoDB 儲存引擎在 MySQL 資料庫中使用最為廣泛,下面以 InnoDB為例子

  • InnoDB 的索引模型

在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種儲存方式的表稱為索引組織表。
InnoDB 使用了 B+ 樹索引模型,所以資料都是儲存在 B+ 樹中的,每一個索引在 InnoDB 裡面對應一棵 B+ 樹。
假設,有一個主鍵列為 ID 的表,表中有欄位 k,並且在 k 上有索引

CREATE TABLE T ( id INT PRIMARY KEY, k INT NOT NULL, NAME VARCHAR ( 16 ), INDEX ( k ) ) ENGINE = INNODB;

表中 R1~R5 的 (ID,k) 值分別為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹 的示例示意圖如下

從圖中不難看出,根據葉子節點的內容,索引型別分為 主鍵索引 和 非主鍵索引 。
主鍵索引的葉子節點存的是整行資料。在 InnoDB 裡,主鍵索引也被稱為聚簇索引 (clustered index)。
非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裡,非主鍵索引也被稱為二級索引 (secondary index)。
根據上面的索引結構說明,來討論一個問題:基於主鍵索引和普通索引的查詢有什麼區別?

如果語句是 select * from T where ID=500,即主鍵查詢方式,則只需要搜尋 ID 這棵 B+ 樹;
如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜尋 k 索引 樹,得到 ID 的值為 500,再到 ID 索引樹搜尋一次。
這個過程稱為回表。

也就是說,基於非主鍵索引的查詢需要多掃描一棵索引樹。因此,在應用中應該儘量使用主鍵查詢。.

  • 索引維護

    B+樹為了維護索引有序性,在插入新值的時候需要做必要的維護。
    以上面這個圖為例,
    如果插入新的行ID值為 700,則只需要在 R5 的記錄後面插入一個新記錄。
    如果新插入的ID值為400,就相對麻煩了,需要邏輯上挪動後面的資料,空出位置。
    而更糟的情況是,如果 R5 所在的資料頁已經滿了,根據 B+ 樹的演算法,這時候需要申請一個新的資料頁,然後挪動部分資料過去。
    這個過程稱為頁分裂。在這種情況下,效能自然會受影響。
    除了效能外,頁分裂操作還影響資料頁的利用率。原本放在一個頁的資料,現在分到兩個頁中,整體空間利用率降低大約50%。

  • 基於上面的索引維護過程說明,討論一個案例:

    在一些建表規範裡面見到過類似的描述,要求建表語句裡一定要有自增主鍵。上一個文章中也提到了這點,這裡再次描述。
    分析一下哪些場景下應該使用自增主鍵,而哪些場景下不應該。

    自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這麼定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
    插入新記錄的時候可以不指定 ID 的值,系統會獲取當前 ID 最大值加 1 作為下一條記錄的 ID 值。

    也就是說,自增主鍵的插入資料模式,正符合了我們前面提到的遞增插入的場景。
    每次插入一條新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。
    而有業務邏輯的欄位做主鍵,則往往不容易保證有序插入,這樣寫資料成本相對較高。

除了考慮效能外,還可以從儲存空間的角度來看。
假表中確實有一個唯一欄位, 比如字串型別的身份證號,那應該用身份證號做主鍵,還是用自增欄位做主鍵呢?

由於每個非主鍵索引的葉子節點上都是主鍵的值(因為要根據非主鍵索引找到主鍵索引位置然後再找到資料,可看上圖)。
如果用身份證號做主鍵,那麼每個二級索引的葉子節點佔用約 20 個位元組,
而如果用整型做主鍵,則只要 4 個位元組,如果是長整型 (bigint)則是 8 個位元組。
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也就越小。
所以,從效能和儲存空間方面考量,自增主鍵往往是更合理的選擇。

什麼場景適合用業務欄位直接做主鍵的呢?有些業務的場景需求是如下:

1. 只有一個索引
2. 該索引必須是唯一索引 這就是典型的 KV 場景。

由於沒有其他索引,所以也就不用考慮其他索引的葉子節點大小的問題。
這時候就要優先考慮上一段提到的“儘量使用主鍵查詢”原則,直接將這個索引設定為主鍵,可以避免每次查詢需要搜尋兩棵樹。

對於上面例子中的 InnoDB 表 T,如果要重建索引k,可以寫:

alter table T drop index k;
alter table T add index(k);

要重建主鍵索引,可以寫

alter table T drop primary key;
alter table T add primary key(id);

這樣寫是否合理?

重建索引 k的做法是合理的,可以達到省空間的目的。
但是,重建主鍵的過程不合理。
不論是刪除主鍵還是建立主鍵,都會將整個表重建。
所以連著執行這兩個語句的話,第一個語句就白做了。
這兩個語句,可以用這個語句代替 :alter table T engine=InnoDB    
  • sql掃描行數的探討

     CREATE TABLE T (
     ID INT PRIMARY KEY,
     k INT NOT NULL DEFAULT 0,
     s VARCHAR ( 16 ) NOT NULL DEFAULT '',
     INDEX k( k ) 
     ) ENGINE = INNODB;
     INSERT INTO T
     VALUES
      ( 100, 1, 'aa' ),
      ( 200, 2, 'bb' ),
      ( 300, 3, 'cc' ),
      ( 500, 5, 'ee' ),
      ( 600, 6, 'ff' ),
      ( 700, 7, 'gg' );
    

這個表 T 中,如果我執行以下sql 需要執行幾次樹的搜尋操作,會掃描多少行?

select * from T where k between 3 and 5;

先來看看這條 SQL 查詢語句的執行流程:

 1. 在 k 索引樹上找到 k=3 的記錄,取得 ID = 300 
 2. 再到 ID 索引樹查到 ID=300 對應的 R3 
 3. 在 k 索引樹取下一個值 k=5,取得 ID=500 
 4. 再回到 ID 索引樹查到 ID=500 對應的 R4 
 5. 在 k 索引樹取下一個值 k=6,不滿足條件,迴圈結束 
     
 在這個過程中,回到主鍵索引樹搜尋的過程,稱為回表。
 可以看到,這個查詢過程讀了k索引樹的3條記錄(步驟 1、3 和 5),回表了兩次(步驟 2 和 4)
 在這個例子中,由於查詢結果所需要的資料只在主鍵索引上有,所以不得不回表。
 那麼,有沒有可能經過索引優化,避免回表過程呢?
 
    答案是覆蓋索引
        
        如果執行的語句是 select ID from T where k between 3 and 5,這時只需要查 ID 的值, 
        而 ID 的值已經在 k 索引樹上了,因此可以直接提供查詢結果,不需要回表。
        也就是說,在這個查詢裡面,索引k已經“覆蓋了”查詢需求,稱為覆蓋索引。

        由於覆蓋索引可以減少樹的搜尋次數,顯著提升查詢效能,所以使用覆蓋索引是一個常用的效能優化手段。
        
        需要注意的是,在引擎內部使用覆蓋索引在索引 k上其實讀了三個記錄,R3~R5(對應的索引 k 上的記錄項)
        但是對於 MySQL 的 Server 層來說,它就是找引擎拿到了兩條記錄,因此 MySQL 認為掃描行數是 2。(這個行數掃描後面需要注意)

基於上面覆蓋索引的說明,討論另一個問題:

在一個市民資訊表上,是否有必要將身份證號和名字建立聯合索引?

假設這個市民表的定義是這樣的:
    CREATE TABLE `tuser` (
    `id` INT ( 11 ) NOT NULL,
    `id_card` VARCHAR ( 32 ) DEFAULT NULL,
    `name` VARCHAR ( 32 ) DEFAULT NULL,
    `age` INT ( 11 ) DEFAULT NULL,
    `ismale` TINYINT ( 1 ) DEFAULT NULL,
    PRIMARY KEY ( `id` ),
    KEY `id_card` ( `id_card` ),
    KEY `name_age` ( `name`, `age` ) 
    ) ENGINE = INNODB;
    
身份證號是市民的唯一標識。
也就是說,如果有根據身份證號查詢市民資訊的需求,只要在身份證號欄位上建立索引就夠了。
而再建立一個(身份證號、姓名)的聯合 索引,是不是浪費空間?
    
如果現在有一個高頻請求,要根據市民的身份證號查詢他的姓名,這個聯合索引就有意義了。
它可以在這個高頻請求上用到覆蓋索引,不再需要回表查整行記錄,減少語句的執行時間。
當然,索引欄位的維護總是有代價的。因此,在建立冗餘索引來支援覆蓋索引時就需要權衡考慮了。
這些是業務 DBA,或者稱為業務資料架構師的工作。
  • 最左字首原則 (後面會詳細介紹)

    看到這裡你一定有一個疑問,如果為每一種查詢都設計一個索引,索引是不是太多了。
    如果我現在要按照市民的身份證號去查他的家庭地址呢?
    雖然這個查詢需求在業務中出現的概率不高,但總不能讓它走全表掃描吧?
    反過來說,單獨為一個不頻繁的請求建立一個(身份證號,地址)的索引又感覺有點浪費。應該怎麼做呢?

    這裡便可以利用b+tree索引的“最左字首原則”

    為了直觀地說明這個概念,這裡用(name,age)這個聯合索引來分析。

     可以看到,索引項是按照索引定義裡面出現的欄位順序排序的。
     
     當你的邏輯需求是查到所有名字是“張三”的人時,可以快速定位到ID4,然後向後遍歷得到所有需要的結果
     
     如果你要查的是所有名字第一個字是“張”的人,你的SQL語句的條件是"where namelike ‘張 %’"。
     這時,你也能夠用上這個索引,查詢到第一個符合條件的記錄是 ID3,然後向後遍歷,直到不滿足條件為止。
     
     可以看到,不只是索引的全部定義,只要滿足最左字首,就可以利用索引來加速檢索。
     這個最左字首可以是聯合索引的最左N個欄位,也可以是字串索引的最左M個字元
    

    基於上面對最左字首索引的說明,那麼在建立聯合索引的時候,如何安排索引內的欄位順序?

     這裡大多的評估標準是,索引的複用能力。
     因為可以支援最左字首,所以當已經有了 (a,b)這個聯合索引後,一般就不需要單獨在 a上建立索引了。
     因此,第一原則是,如果通過調整順序,可以少維護一個索引,那麼這個順序往往就是需要優先考慮採用
     
     所以現在可以知道了,這段開頭的問題裡,要為高頻請求建立 (身份證號,姓名)這個聯合索引,並用這個索引支援“根據身份證號查詢地址”的需求
     
     那麼,如果既有聯合查詢,又有基於 a、b 各自的查詢呢?查詢條件裡面只有 b 的語句,是無法使用 (a,b) 這個聯合索引的,
     這時候你不得不維護另外一個索引,也就是說需要同時維護 (a,b)、(b) 這兩個索引。
     這時候,要考慮的原則就是空間了。
     比如上面這個市民表的情況,name 欄位是比age 欄位大的 ,那建議建立一個(name,age) 的聯合索引和一個 (age) 的單欄位索引。
    
  • 索引下推

    上一段說到滿足最左字首原則的時候,最左字首可以用於在索引中定位記錄。那麼那些不符合最左字首的部分,會怎麼樣呢?

      還是以市民表的聯合索引(name, age)為例。如果現在有一個需求:
      檢索出表中“名字第一個字是張,而且年齡是 10歲的所有男孩”。那麼,SQL 語句是這麼寫的
          select * from tuser where name like '張 %' and age=10 and ismale=1;
      
      已經知道了字首索引規則,所以這個語句在搜尋索引樹的時候,只能用 “張”,找到第一個滿足條件的記錄 ID3。
      這還不錯,總比全表掃描要好。然後判斷其他條件是否滿足。
      
      在 MySQL 5.6 之前,只能從 ID3 開始一個個回表。到主鍵索引上找出資料行,再對比欄位值
      
      而 MySQL 5.6 引入的索引下推優化(index condition pushdown),可以在索引遍歷過程中,
      對索引中包含的欄位先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數
      
      看下圖分析
    

  • tip

    實際上主鍵索引也是可以使用多個欄位的。
    假如DBA小呂在入職新公司的時候,就發現自己接手維護的庫裡面,有這麼一個表,表結構定義類似這樣的

      CREATE TABLE `geek` (
      `a` INT ( 11 ) NOT NULL,
      `b` INT ( 11 ) NOT NULL,
      `c` INT ( 11 ) NOT NULL,
      `d` INT ( 11 ) NOT NULL,
      PRIMARY KEY ( `a`, `b` ),
      KEY `c` ( `c` ),
      KEY `ca` ( `c`, `a` ),
      KEY `cb` ( `c`, `b` ) 
      ) ENGINE = INNODB;
    
       公司的同事告訴他說,由於歷史原因,這個表需要 a、b 做聯合主鍵,這個小呂理解了
       可是根據上面提到的內容,主鍵包含了 a、b 這兩個欄位,那意味著單獨在欄位 c 上建立一個索引,
       就已經包含了三個欄位了,為什麼要建立“ca”“cb”這兩個索引?
       
       同事告訴他,是因為他們的業務裡面有這樣的兩種語句:
          select * from geek where c=N order by a limit 1;
          select * from geek where c=N order by b limit 1;
       
     為了這兩個查詢模式,這兩個索引是否都是必須的?為什麼呢?
     
       假如表記錄
       –a--|–b--|–c--|–d--
       1 2 3 d
       1 3 2 d
       1 4 3 d
       2 1 3 d
       2 2 2 d
       2 3 4 d
       主鍵 a,b的聚簇索引組織順序相當於 order by a,b ,也就是先按 a 排序,再按 b 排序,c 無序。
       
       索引 ca 的組織是先按 c排序,再按 a 排序,同時記錄主鍵
       –c--|–a--|–主鍵部分b-- 
       2 1            3
       2 2            2
       3 1            2
       3 1            4
       3 2            1
       4 2            3
       這個跟索引 c 的資料是一模一樣的。
       
       索引 cb 的組織是先按 c 排序,再按 b 排序,同時記錄主鍵
       –c--|–b--|–主鍵部分a--
       2 2            2
       2 3            1
       3 1            2
       3 2            1
       3 4            1
       4 3            2
       
       ca索引可以去掉,cb索引可以保留。
       ca索引,通過索引對資料進行篩選,回表的時候,a本身就是主鍵索引,所以可以保證有序;
       cb索引,b上並沒有索引,ab索引也無法滿足最左匹配原則,可以保留加快排序速度。
       包含主鍵後應該是cab,根據最左匹配原則,cb是有必要的,ca沒有必要
       所以,結論是 ca 可以去掉,cb 需要保留。
    

4. Explain詳解

  • Explain

     使用EXPLAIN關鍵字可以模擬優化器執行SQL語句,分析你的查詢語句或是結構的效能瓶頸在select語句之前增加explain關鍵字,
     MySQL會在查詢上設定一個標記,執行查詢會返回執行計劃的資訊,而不是執行這條SQL
     注意:如果from中包含子查詢,仍會執行該子查詢,將結果放入臨時表中
     
     
     drop table if exists actor;
     CREATE TABLE `actor` (
       `id` int(11) NOT NULL AUTO_INCREMENT,
       `name` varchar(45)  NOT NULL,
       `update_time` datetime(6) DEFAULT NULL,
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (1, 'a', '2020-12-29 22:23:44.000000');
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (2, 'b', '2020-12-29 22:23:44.000000');
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (3, 'c', '2020-12-29 22:23:44.000000');
     
     drop table if exists film;
      CREATE TABLE film (
       id int(11) NOT NULL AUTO_INCREMENT,
       name varchar(10)  NOT NULL,
       PRIMARY KEY (id),
         key (name)
     ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO film(id,name) values(1,'film1');
     INSERT INTO film(id,name) values(2,'film2');
     INSERT INTO film(id,name) values(3,'film0');
     
     drop table if exists film_actor;
     CREATE TABLE film_actor (
     id int(11) not null,
     film_id int(11) not null,
     actor_id int(11) not null,
     remark VARCHAR(255) null,
     PRIMARY key(id),
     KEY idx_film_actor_id(film_id,actor_id) 
     )ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO film_actor(id,film_id,actor_id) values(1,1,1);
     INSERT INTO film_actor(id,film_id,actor_id) values(2,1,2);
     INSERT INTO film_actor(id,film_id,actor_id) values(3,2,1);
     
     explain select * from actor;
    

    在查詢中的每個表會輸出一行,如果有兩個表通過join連線查詢,那麼會輸出兩行,輸出數字越高,執行越先
  • explain兩個變種

     explainextended
     
         會在 explain 的基礎上額外提供一些查詢優化的資訊。
         緊隨其後通過 showwarnings 命令可以得到優化後的查詢語句,從而看出優化器優化了什麼。
         額外還有filtered列,是一個半分比的值,
         rows*filtered/100
             可以估算出將要和explain中前一個表進行連線的行數(前一個表指explain中的id值比當前表id值小的表)
         
         explain extended select * from film where id = 1;
    

         show warnings;
    

     explainpartitions
         
         相比explain多了個partitions欄位,如果查詢是基於分割槽表的話,會顯示查詢將訪問的分割槽。
    
  • explain中的列介紹

  • id

    id列的編號是select的序列號,有幾個select就有幾個id,並且id的順序是按select出現的順序增長的。
    id列越大執行優先順序越高,id相同則從上往下執行,id為NULL最後執行。

  • select_type

    select_type 表示對應行是簡單還是複雜的查詢,其中還分為五種型別

     1. simple:簡單查詢。查詢不包含子查詢和union
         explain select * from film where id = 2
     2. primary:複雜查詢中最外層的select
     3. subquery:包含在select中的子查詢(不在from子句中)
     4. derived:包含在from子句中的子查詢。
        MySQL會將結果存放在一個臨時表中,也稱為派生表(derived的英文含義)
        
        #在執行前 關閉mysql5.7新特性對衍生表的合併優化 之後關閉
        set session optimizer_switch='derived_merge=off'
        explain select (select 1 from actor where id = 1) from (select * from film where id = 1)der;
        set session optimizer_switch='derived_merge=on'
        
     5. union:在union中的第二個和隨後的select
         explain select 1 union all select 1 ;
    

  • table

     這一列表示explain的一行正在訪問哪個表。
     當from子句中有子查詢時,table列是<derivenN>格式,表示當前查詢依賴id=N的查詢,於是先執行id=N的查詢。
     當有union時,UNIONRESULT的table列的值為<union1,2>,1和2表示參與union的select行id。
    
  • type

     這一列表示關聯型別或訪問型別,即MySQL決定如何查詢表中的行,查詢資料行記錄的大概範圍。
     依次從最優到最差分別為:system>const>eq_ref>ref>range>index>ALL
     一般來說,得保證查詢達到range級別,最好達到ref
     NULL: mysql能夠在優化階段分解查詢語句,在執行階段用不著再訪問表或索引。
     例如:在索引列中選取最小值,可以單獨查詢索引來完成,不需要在執行時訪問表
     
     explain select min(id) from film
    

    const,system
        mysql能對查詢的某部分進行優化並將其轉化成一個常量(可以看showwarnings的結果)。
        用於primarykey或uniquekey的所有列與常數比較時,所以表最多有一個匹配行,讀取1次,速度比較快。
        system是const的特例,表裡只有一條元組匹配時為system
        
    explain extended select * from (select * from film where id= 1) tmp;
    使用showwarnings;可以看到Message 已經是直接select常量了

    eq_ref
        primarykey或uniquekey索引的所有部分被連線使用,最多隻會返回一條符合件的記錄。
        這可能是在const之外最好的聯接型別了,簡單的select查詢不會出現這種type。
   
     explain select * from film_actor left join film on film_actor.film_id = film.id;

    ref
        相比eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分字首,索引要和某個值相比較,可能會找到多個符合條件的行。
        
        1. 簡單select查詢,name是普通索引(非唯一索引)
           explain select * from film where name = 'film1';
            
        2.關聯表查詢,idx_film_actor_id是film_id和actor_id的聯合索引,
          這裡使用到了film_actor的左邊字首film_id部分
          explain select film_id from film left join film_actor on film.id = film_actor.film_id;

    range
        範圍掃描通常出現在in(),between,>,<,>=等操作中。使用一個索引來檢索給定範圍的行。
        explain select * from actor where id > 1;
        
    index
        掃描全表索引,這通常比ALL快一些
    
    ALL
        即全表掃描,意味著mysql需要從頭到尾去查詢所需要的行。通常情況下這需要增加索引來進行優化了  

  • possible_keys

     這一列顯示查詢可能使用哪些索引來查詢。
     explain時可能出現possible_keys有列,而key顯示NULL的情況,
     這種情況是因為表中資料不多,mysql認為索引對此查詢幫助不大,選擇了全表查詢。
     如果該列是NULL,則沒有相關的索引。
     在這種情況下,可以通過檢查where子句看是否可以創造一個適當的索引來提高查詢效能,然後用explain檢視效果
    
  • key

     這一列顯示mysql實際採用哪個索引來優化對該表的訪問。如果沒有使用索引,則該列是NULL。
     如果想強制mysql使用或忽視possible_keys列中的索引,在查詢中使用force index、ignore index
    
  • key

     這一列顯示了mysql在索引裡使用的位元組數,通過這個值可以算出具體使用了索引中的哪些列。
     舉例來說,film_actor的聯合索引idx_film_actor_id由film_id和actor_id兩個int列組成,
     並且每個int是4位元組。通過結果中的key_len=4可推斷出查詢使用了第一個列:film_id列來執行索引查詢。
     
     key_len計算規則如下:
         字串
             char(n):n位元組長度
             varchar(n):2位元組儲存字串長度,如果是utf-8,則長度: 3n+2
         數值型別
             tinyint:1位元組
             smallint:2位元組
             int:4位元組
             bigint:8位元組 
         時間型別
             date:3位元組
             timestamp:4位元組
             datetime:8位元組
     如果欄位允許為NULL,需要1位元組記錄是否為NULL
     索引最大長度是768位元組,當字串過長時,mysql會做一個類似左字首索引的處理,將前半
     部分的字元提取出來做索引
     
     explain select * from film_actor where film_id = 2
    

  • ref

     這一列顯示了在key列記錄的索引中,表查詢值所用到的列或常量,常見的有:const(常量),欄位名(例:film.id)
    
  • rows

         這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裡的行數(這個是包括索引掃描、回表等加起來的)
    
  • Extra列

         1. Using index 
             使用覆蓋索引
             explain select film_id from film_actor where film_id = 1;
             
         2. Usingwhere
             使用where語句來處理結果,查詢的列未被索引覆蓋
             explain select * from actor where name = 'a';
             
         3. Usingindexcondition
            查詢的列不完全被索引覆蓋,where條件中是一個前導列的範圍;
            explain select * from film_actor where film_id > 1;
    

    4. Usingtemporary
       mysql需要建立一張臨時表來處理查詢。出現這種情況一般是要進行優化的,首先是想到用索引來優化。
       
       1. actor.name沒有索引,此時建立了張臨時表來distinct
          explain select distinct name from actor;
        
       2. film.name建立了idx_name索引,此時查詢時extra是 usingindex,沒有用臨時表
          explain select distinct name from film;
          
     5. Usingfilesort
        將用外部排序而不是索引排序,資料較小時從記憶體排序,否則需要在磁碟完成排序。
        這種情況下一般也是要考慮使用索引來優化的。
        
        1. actor.name未建立索引,會瀏覽actor整個表,儲存排序關鍵字name和對應的id,然後排序name並檢索行記錄
            explain select * from actor order by name;
            
        2. film.name建立了idx_name索引,此時查詢時 extra是 usingindex
             explain select * from film order by name;

     6. Select tablesoptimizedaway 
        使用某些聚合函式(比如max、min)來訪問存在索引的某個欄位
        
        explain select min(id) from film;

5. mysql索引最佳實踐

  •  CREATE TABLE employees (
         id INT ( 11 ) NOT NULL AUTO_INCREMENT,
         NAME VARCHAR ( 24 ) NOT NULL DEFAULT '',
         age INT ( 11 ) NOT NULL DEFAULT 0,
         position VARCHAR ( 20 ) NOT NULL DEFAULT '',
         hire_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
         PRIMARY KEY ( id ),
         KEY idx_name_age_position ( NAME, age, position ) USING BTREE 
     )ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
     insert into employees(name,age,position) values('LiLei',22,'manager');
     insert into employees(name,age,position) values('HanMeimei',23,'dev');
     insert into employees(name,age,position) values('Lucy',23,'dev');
    
  • 1.全值匹配

     -- idx_name_age_position ( name, age, position ) 簡歷索引的順序
     -- 如果索引了多列,要遵守最左字首法則。指的是查詢從索引的最左前列開始並且不跳過索引中的列
     -- 根據建立索引的順序 name, age, position 以下6條都會走索引,其中第三條順序變了,但是mysql會優化調整順序
     -- 所以也是會走索引 但是前提是其中必須出現最左的索引(name) 不然的話是不會走索引的,比如第八條sql
     explain select * from employees where name = 'LiLei';
     explain select * from employees where name = 'LiLei' and age = 22;
     explain select * from employees where age = 22 and name = 'LiLei';
     explain select * from employees where name = 'LiLei' and age = 22 and position = 'manager';
    

     -- 這條sql第一個しname,符合了原則,但是position是在第三個的,中間漏了age,所以不是全部走了索引,使用的是  Using index condition
     explain select * from employees where name = 'LiLei' and position = 'manager';
     explain select * from employees where position = 'manager' and name = 'LiLei';
    
  • 2.最左字首

     如果索引了多列,要遵守最左字首法則。指的是查詢從索引的最左前列開始並且不跳過索引中的列
     
     -- 以下2條sql沒有遵守最左匹配原則 使用的是 Using where
     explain select * from employees where age = 22;
     explain select * from employees where age = 22 and position = 'manager';
    

  • 3.索引列不要使用函式

     不要在索引列上做任何操作(計算、函式、(自動or手動)型別轉換),會導致索引失效而轉向全表掃描
     
     explain select * from employees where name = 'LiLei';
     explain select * from employees where left(name,3) = 'LiLei';
     
     
     給hire_time增加一個普通索引,然後使用函式包裝查詢
     alter table `employees` add index `idx_hire_time` (`hire_time`) using btree;
     explain select * from employees where date(hire_time) = '2018-09-30';
     
     優化為日期範圍查詢,走索引
     explain select * from employees where hire_time >= '2018-09-30 00:00:00' and hire_time <= '2018-09-30 23:59:59';
     
     刪除索引
     alter table `employees` drop index `idx_hire_time`;
    

  • 4.索引中範圍條件右邊的列無法使用索引

     儲存引擎不能使用索引中範圍條件右邊的列
     
     explain select * from employees where name = 'LiLei' and age = 22 AND position = 'manager';
     explain select * from employees where name = 'LiLei' and age > 22 AND position = 'manager';
    

  • 5.使用覆蓋索引

     儘量使用覆蓋索引(只訪問索引的查詢(索引列包含查詢列)),減少select*語句
     
     explain select name,age from employees where name = 'LiLei' AND age = 23 AND position = 'manager';
     explain select * from employees where name = 'LiLei' AND age = 23 AND position = 'manager';
    

-6.使用不等於(!=或者<>),is nul is not null 的時候無法使用索引

    mysql在使用不等於(!=或者<>)的時候無法使用索引會導致全表掃描
    isnull,isnotnull也無法使用索引
    
    explain select * from employees where name != 'LiLei';
    explain select * from employees where name is null;

-7.like以萬用字元開頭('$abc...')mysql索引失效會變成全表掃描操作

    explain select * from employees where name like '%Lei';
    
    如何解決like'%字串%'索引不被使用的方法?
        1.使用覆蓋索引,查詢欄位必須是建立覆蓋索引欄位
            explain select name,age,position from employees where name like '%Lei%';
        2.如果不能使用覆蓋索引則可能需要藉助搜尋引擎
            easysearch等
            
     like KK% 相當於=常量,%KK和%KK%相當於範圍

  • 8.字串不加單引號索引失效

     底層加了函式進行轉換,使用了函式,無法使用索引
     
    
     explain select * from employees where name = '1000';
     explain select * from employees where name = 1000;
    

-9.少用or或in,用它查詢時,mysql不一定使用索引,

    mysql內部優化器會根據檢索比例、表大小等多個因素整體評估是否使用索引,詳見範圍查詢優化
    
    explain select * from employees where name = 'LiLei' or name = 'HanMeimei';

-10.範圍查詢優化

    給年齡新增單值索引
    alter table `employees` add index `idx_age` (`age`) using btree;
    
    explain select * from employees where age >=1 and age <=2000;
    
    沒走索引原因:(不一定)
        mysql內部優化器會根據檢索比例、表大小等多個因素整體評估是否使用索引。
        比如這個例子,可能是由於單次資料量查詢過大導致優化器最終選擇不走索引
    優化方法:可以講大的範圍拆分成多個小範圍
    
    -- 資料庫中一共就三條資料22,23,23
    -- 這條查詢不會走索引 idx_age 因為所有資料都會返回
    explain select * from employees where age >=22 and age <=1000;
    -- 這條會走索引 idx_age
    explain select * from employees where age >=23 and age <=1000;
    -- 這條會走索引 idx_age
    explain select * from employees where age >= 1001 and age <= 2000;
    
    alter table `employees` drop index `idx_age`;

6. Mysql如何選擇合適的索引

  • 索引覆蓋的實踐優化

      explain select * from employees where name > 'a';
      
      以上sql,如果用name索引需要遍歷name欄位聯合索引樹,然後還需要根據遍歷出來的主鍵值去主鍵索引樹裡再去查出最終資料,
      成本比全表掃描還高,可以用覆蓋索引優化,這樣只需要遍歷name欄位的聯合索引樹就能拿到所有結果,如下:
      
      explain select * from employees where name > 'a';
      
      explain select name,age,position from employees where name > 'a';
       
      explain select * from employees where name > 'zzz';
      
      
      對於上面這兩種name>'a'和name>'zzz'的執行結果,mysql最終是否選擇走索引或者一張表涉及多個索引,
      mysql最終如何選擇索引,我們可以用trace工具來一查究竟,開啟trace工具會影響mysql效能,
      所以只能臨時分析sql使用,用完之後立即關閉
         
        set session optimizer_trace ="enabled=on",end_markers_in_json=on;‐‐開啟trace
        select * from employees where name > 'a' order by position;
        select * from information_schema.OPTIMIZER_TRACE;
        
        
       "steps": [
         {
           /* ‐‐第一階段:SQL準備階段 */
           "join_preparation": {
             "select#": 1,
             "steps": [
               {
                 "expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`NAME` AS `NAME`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`NAME` > 'a') order by `employees`.`position`"
               }
             ] /* steps */
           } /* join_preparation */
         },
         {
           /* 第二階段:SQL優化階段 */
           "join_optimization": {
             "select#": 1,
             "steps": [
               {
                 /* ‐‐條件處理 */
                 "condition_processing": {
                   "condition": "WHERE",
                   "original_condition": "(`employees`.`NAME` > 'a')",
                   "steps": [
                     {
                       "transformation": "equality_propagation",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     },
                     {
                       "transformation": "constant_propagation",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     },
                     {
                       "transformation": "trivial_condition_removal",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     }
                   ] /* steps */
                 } /* condition_processing */
               },
               {
                 "substitute_generated_columns": {
                 } /* substitute_generated_columns */
               },
               {
                 /* 表依賴詳情 */
                 "table_dependencies": [
                   {
                     "table": "`employees`",
                     "row_may_be_null": false,
                     "map_bit": 0,
                     "depends_on_map_bits": [
                     ] /* depends_on_map_bits */
                   }
                 ] /* table_dependencies */
               },
               {
                 "ref_optimizer_key_uses": [
                 ] /* ref_optimizer_key_uses */
               },
               {
                 /* 預估表的訪問成本 */
                 "rows_estimation": [
                   {
                     "table": "`employees`",
                     "range_analysis": {
                       /* 全表掃描情況 */
                       "table_scan": {
                         "rows": 3,   ‐‐掃描行數
                         "cost": 3.7 ‐‐查詢成本
                       } /* table_scan */,
                       "potential_range_indexes": [ ‐‐查詢可能使用的索引
                         {
                           "index": "PRIMARY",      ‐‐主鍵索引
                           "usable": false,
                           "cause": "not_applicable"
                         },
                         {
                           "index": "idx_name_age_position", ‐‐輔助索引
                           "usable": true,
                           "key_parts": [
                             "NAME",
                             "age",
                             "position",
                             "id"
                           ] /* key_parts */
                         },
                         {
                           "index": "idx_age",
                           "usable": false,
                           "cause": "not_applicable"
                         }
                       ] /* potential_range_indexes */,
                       "setup_range_conditions": [
                       ] /* setup_range_conditions */,
                       "group_index_range": {
                         "chosen": false,
                         "cause": "not_group_by_or_distinct"
                       } /* group_index_range */,
                       "analyzing_range_alternatives": { ‐‐分析各個索引使用成本
                         "range_scan_alternatives": [
                           {
                             "index": "idx_name_age_position",
                             "ranges": [    ‐‐索引使用範圍
                               "a < NAME"
                             ] /* ranges */,
                             "index_dives_for_eq_ranges": true,
                             "rowid_ordered": false,  ‐‐使用該索引獲取的記錄是否按照主鍵排序
                             "using_mrr": false,
                             "index_only": false,     ‐‐是否使用覆蓋索引
                             "rows": 3,               ‐‐索引掃描行數
                             "cost": 4.61,            ‐‐索引使用成本
                             "chosen": false,         ‐‐是否選擇該索引
                             "cause": "cost"
                           }
                         ] /* range_scan_alternatives */,
                         "analyzing_roworder_intersect": {
                           "usable": false,
                           "cause": "too_few_roworder_scans"
                         } /* analyzing_roworder_intersect */
                       } /* analyzing_range_alternatives */
                     } /* range_analysis */
                   }
                 ] /* rows_estimation */
               },
               {
                 "considered_execution_plans": [
                   {
                     "plan_prefix": [
                     ] /* plan_prefix */,
                     "table": "`employees`",
                     "best_access_path": {     ‐‐最優訪問路徑
                       "considered_access_paths": [   ‐‐最終選擇的訪問路徑
                         {
                           "rows_to_scan": 3,
                           "access_type": "scan",    ‐‐訪問型別:為scan,全表掃描
                           "resulting_rows": 3,
                           "cost": 1.6,
                           "chosen": true,           ‐‐確定選擇
                           "use_tmp_table": true
                         }
                       ] /* considered_access_paths */
                     } /* best_access_path */,
                     "condition_filtering_pct": 100,
                     "rows_for_plan": 3,
                     "cost_for_plan": 1.6,
                     "sort_cost": 3,
                     "new_cost_for_plan": 4.6,
                     "chosen": true
                   }
                 ] /* considered_execution_plans */
               },
               {
                 "attaching_conditions_to_tables": {
                   "original_condition": "(`employees`.`NAME` > 'a')",
                   "attached_conditions_computation": [
                   ] /* attached_conditions_computation */,
                   "attached_conditions_summary": [
                     {
                       "table": "`employees`",
                       "attached": "(`employees`.`NAME` > 'a')"
                     }
                   ] /* attached_conditions_summary */
                 } /* attaching_conditions_to_tables */
               },
               {
                 "clause_processing": {
                   "clause": "ORDER BY",
                   "original_clause": "`employees`.`position`",
                   "items": [
                     {
                       "item": "`employees`.`position`"
                     }
                   ] /* items */,
                   "resulting_clause_is_simple": true,
                   "resulting_clause": "`employees`.`position`"
                 } /* clause_processing */
               },
               {
                 "reconsidering_access_paths_for_index_ordering": {
                   "clause": "ORDER BY",
                   "steps": [
                   ] /* steps */,
                   "index_order_summary": {
                     "table": "`employees`",
                     "index_provides_order": false,
                     "order_direction": "undefined",
                     "index": "unknown",
                     "plan_changed": false
                   } /* index_order_summary */
                 } /* reconsidering_access_paths_for_index_ordering */
               },
               {
                 "refine_plan": [
                   {
                     "table": "`employees`"
                   }
                 ] /* refine_plan */
               }
             ] /* steps */
           } /* join_optimization */
         },
         {
           "join_execution": {  ‐‐第三階段:SQL執行階段
             "select#": 1,
             "steps": [
               {
                 "filesort_information": [
                   {
                     "direction": "asc",
                     "table": "`employees`",
                     "field": "position"
                   }
                 ] /* filesort_information */,
                 "filesort_priority_queue_optimization": {
                   "usable": false,
                   "cause": "not applicable (no LIMIT)"
                 } /* filesort_priority_queue_optimization */,
                 "filesort_execution": [
                 ] /* filesort_execution */,
                 "filesort_summary": {
                   "rows": 3,
                   "examined_rows": 3,
                   "number_of_tmp_files": 0,
                   "sort_buffer_size": 262080,
                   "sort_mode": "<sort_key, packed_additional_fields>"
                 } /* filesort_summary */
               }
             ] /* steps */
           } /* join_execution */
         }
       ] /* steps */
     }
    
     結論:全表掃描的成本低於索引掃描,所以mysql最終選擇全表掃描
     
     select * from employees where name > 'zzz' order by position;
     select * from information_schema.OPTIMIZER_TRACE;
     檢視trace欄位可知索引掃描的成本低於全表掃描,所以mysql最終選擇索引掃描
     
     set session optimizer_trace ="enabled=on",end_markers_in_json=off;‐‐關閉trace