深刻淺出mysql優化--一篇博客讓你精通mysql優化策略--上

一篇博客和你們一塊兒學習mysql優化的通用策略。
內容花了好些時間來整理書寫,若是以爲有用,還請點個贊,還有就是發現博客園的MD文本格式和在其餘軟件的不太同,格式調的也很差,還請將就一下
接下來一塊兒來學習一下mysql優化的內容吧(注意 本文中使用的字符集是4個長度的)mysql

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

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

    MySQL 的邏輯架構圖.jpg

    大致來講,MySQL能夠分爲 Server層 和 存儲引擎層 兩部分sql

    Server層:
    包括鏈接器、查詢緩存、分析器、優化器、執行器等,涵蓋 MySQL 的大多數核心服務功能,
    以及全部的內置函數(如日期、時間、數學和加密函數等),全部跨存儲引擎的功能都在這一層實現,
    好比存儲過程、觸發器、視圖等數據庫

    存儲引擎層:
    負責數據的存儲和提取。
    其架構模式是插件式的,支持 InnoDB、MyISAM、Memory 等多個存儲引擎。
    如今最經常使用的存儲引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成爲了默認存儲引擎json

    執行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」的這一行,就表示如今系統裏面有一個空閒鏈接session

showprocresslist.jpg

客戶端若是太長時間沒動靜,鏈接器就會自動將它斷開。
 這個時間是由參數 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 redo log.jpg

    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 內部執行的,深色框表示是在執行器中執行的

update 語句的執行流程圖.jpg

將 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條記錄(步驟 一、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),能夠在索引遍歷過程當中,
      對索引中包含的字段先作判斷,直接過濾掉不知足條件的記錄,減小回表次數
      
      看下圖分析

    索引下推執行流程.jpg

  • 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;

圖1

在查詢中的每一個表會輸出一行,若是有兩個表經過 join 鏈接查詢,那麼會輸出兩行,輸出數字越高,執行越先
  • explain 兩個變種

    explain extended
     
         會在 explain 的基礎上額外提供一些查詢優化的信息。
         緊隨其後經過 show warnings 命令能夠獲得優化後的查詢語句,從而看出優化器優化了什麼。
         額外還有filtered列,是一個半分比的值,
         rows*filtered/100 
             能夠估算出將要和explain中前一個表進行鏈接的行數(前一個表指explain中的id值比當前表id值小的表)
         
         explain extended select * from film where id = 1;

    圖2

    show warnings;

    圖3

    explain partitions
         
         相比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 ;

    圖4

  • table

    這一列表示explain的一行正在訪問哪一個表。
     當from子句中有子查詢時,table列是 <derivenN> 格式,表示當前查詢依賴 id=N的查詢,因而先執行id=N的查詢。
     當有union時,UNION RESULT的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

圖5

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

圖6

eq_ref
        primary key或unique key索引的全部部分被鏈接使用,最多隻會返回一條符合件的記錄。
        這多是在const以外最好的聯接類型了,簡單的select查詢不會出現這種type。
   
     explain select * from film_actor left join film on film_actor.film_id = film.id;

圖7

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;

圖8

range
        範圍掃描一般出如今in(), between ,> ,<, >= 等操做中。使用一個索引來檢索給定範圍的行。
        explain select * from actor where id > 1;
        
    index
        掃描全表索引,這一般比ALL快一些
    
    ALL
        即全表掃描,意味着mysql須要從頭至尾去查找所須要的行。一般狀況下這須要增長索引來進行優化了

圖9

  • 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

圖10

  • ref

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

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

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

圖11

4. Using temporary
       mysql須要建立一張臨時表來處理查詢。出現這種狀況通常是要進行優化的,首先是想到用索引來優化。
       
       1. actor.name沒有索引,此時建立了張臨時表來distinct
          explain select distinct name from actor;
        
       2. film.name創建了idx_name索引,此時查詢時extra是 using index,沒有用臨時表
          explain select distinct name from film;
          
     5. Using filesort
        將用外部排序而不是索引排序,數據較小時從內存排序,不然須要在磁盤完成排序。
        這種狀況下通常也是要考慮使用索引來優化的。
        
        1. actor.name未建立索引,會瀏覽actor整個表,保存排序關鍵字name和對應的id,而後排序name並檢索行記錄
            explain select * from actor order by name;
            
        2. film.name創建了idx_name索引,此時查詢時 extra是 using index
             explain select * from film order by name;

圖12

6. Select tables optimized away 
        使用某些聚合函數(好比 max、min)來訪問存在索引的某個字段
        
        explain select min(id) from film;

圖13

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';

    圖14

    -- 這條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';

    圖15

  • 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`;

圖16

  • 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';

圖17

  • 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在使用不等於(!=或者<>)的時候沒法使用索引會致使全表掃描
    is null,is not null 也沒法使用索引
    
    explain select * from employees where name != 'LiLei';
    explain select * from employees where name is null;

圖19

-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% 至關於範圍

圖20

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

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

圖21

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

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

圖22

-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`;

圖23

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

圖24

相關文章
相關標籤/搜索