本文是對SQL優化的複習總結,主要記錄如何使用索引優化SQL,數據庫爲MySQL。主要從三個部分依次進行探討。 第一部分:理解MySQL索引底層數據結構。 第二部分:SQL分析工具Explain詳解。 第三部分:MySQL的索引最佳實踐。
強烈建議:因爲本文篇幅較長,內容較多。推薦讀者每次僅閱讀一部分,請勿一次性讀完(並不利於消化吸取,大佬除外)。html
索引就是一種幫助MySQL高效獲取數據的排好序的數據結構。
這裏先說結論,MySQL索引的數據結構是B+TREE。 咱們再依次從二叉樹到B+TREE,逐步的理解MySQL索引爲何使用B+TREE。 在開始討論具體的數據結構以前,咱們應該先初步的瞭解什麼是數據結構,數據結構的做用是什麼? 若是你們瞭解設計模式,或者看過個人《MO_or的單例模式複習總結》就能知道。 設計模式是提供了針對不一樣類型的問題的優質解決方案。 相對應的,數據結構其實就是提供了針對不一樣數據的優質存儲方案, 固然這些方案同設計模式同樣,也是經過前輩們無數次的實踐試錯改良所得出的。 那麼下面就正式進入幾種數據結構的講解。
在瞭解二叉樹以前,咱們須要先明白索引爲何要用數據結構? 結合下圖,假如咱們在不使用數據結構(圖片左側)狀況下,須要讀取Col2=89,那麼磁盤就須要進行6次I/O。 若是使用二叉樹來存儲數據(圖片右側),那磁盤僅需進行2此I/O,這就顯著的減小了I/O次數,其效率也就相應獲得了提高。 由此咱們就能明白爲何索引須要使用數據結構。那索引爲何不使用二叉樹,而要使用B+TREE呢?
上圖的右側即是一個常見的二叉樹模型(模型演示的網址在5、參考)。 二叉樹的規則爲下一節點左邊的元素小於上一節點元素(22<34), 下一節點右邊的元素大於等於上一節點元素(89>=34)。 結合下圖,咱們就能明白爲何索引不使用二叉樹。
從上圖中便能看出,當元素都爲依次遞增的狀況下,二叉樹的元素節點則變成了按單列的方式排布。 這時咱們若想取元素6時,那麼也只能讓磁盤進行6次I/O。 因而爲了解決這個問題,咱們就可使用紅黑樹(平衡二叉樹)。
直接上圖,一樣是元素依次遞增的狀況下。
能夠看出紅黑樹在基於二叉樹的基礎上,進行了平衡,再也不是以單列的方式進行排布。 但索引爲何依然沒有使用紅黑樹?由於目前數據量較小,層級不高,但數據庫中一般會出現幾十萬、幾百萬乃至上千萬的數據。 咱們能夠估算下,若表中存儲的數據有100萬條,既2^n=100萬,n=log(2)(100萬),n≈20。 這意味着若咱們須要取得數在最深的節點上,那麼就須要讀寫20次及以上的I/O。 由此咱們能夠看出,紅黑樹在遇到大數據量時,性能依舊較差。並不符合索引能夠高效獲取數據的這一特色。
爲了解決紅黑樹在存儲大量數據的狀況下,層級依舊很深的問題。因而就有了更好方案,B-TREE。那麼咱們先看下B-TREE的模型吧。
從上圖能夠直觀的看出,在紅黑樹的基礎上。B-TREE的葉節點(1五、5六、77所相似的行),從原來只能存儲一個元素,變爲了能夠存儲多個元素。 這樣就大大增長了每一個葉節點的利用空間,減小了層數。但咱們也看到每一個數字節點下方還有個data。 那麼新的問題便產生了,這個data即爲所存儲的數據,那麼當一行數據過大時,每一個葉節點所能容納的元素就相應減小了。 咱們能夠經過如下SQL,來查詢葉節點的大小,一般爲16kb, SHOW GLOBAL STATUS LIKE 'INNODB_page_size'; 若假設一個data爲1kb,那意味着每一個葉節點最多能容納16個元素。那麼當數據量過多時上千萬,依然存在紅黑樹同樣的問題。
那麼終於輪到B+TREE上場了,咱們經過下圖一塊兒看看B+TREE是如何巧妙地解決B-TREE所面臨的問題的吧。
能夠看出,B+TREE非葉子節點(葉子節點爲最下面的一行)是沒有存儲data的,而是存儲索引(冗餘)。 只有葉子節點才存儲data,而且包含了全部的索引。那麼這樣作的意義是什麼呢? 上面說了葉節點的大小一般爲16KB。若索引(冗餘)爲bigint,再加上空白(鏈接箭頭的起始位置實際上是指針),即8b+6b=14b(估算)。 那麼每一個葉節點所能容納的元素個數:16kb=16*1024b,n=16*1024/14≈1170。那就表示葉子節點大約能夠存儲1170個元素。 再假設data爲1kb,同B-TREE,那就是能夠容納16個元素,那麼非葉子節點總共就有:1170*1170*16≈2200萬個元素。 一般來講B+TREE的層次就是2~4層,上千萬的數據量也僅需2~4次I/O就能準肯定位。 在大數據量的狀況下依舊能高效的獲取數據,這即是索引的底層數據結構爲B+TREE的緣由。
這裏僅簡單歸納其原理,具體如何在SQL中體現的,將結合3、Explain的部分進行解讀。 當使用聯合索引(由多個列組成的索引)時,查詢需聽從從左到右的順序,且不能跳過中間的列。不然會致使索引失效。
在上一部分中,咱們對索引有了較爲深刻的理解了,但並不要着急,這一部分暫時還不會詳細的探討如何利用索引優化SQL。 在此以前,咱們還須要瞭解分析SQL性能的一個工具,即Explain。 使用Explain關鍵字,能夠模擬優化器執行SQL語句,分析查詢語句或結構的性能瓶頸。咱們能夠根據分析結果,進行對應的優化。 那麼如今結合SQL咱們來看看Explain吧。
-- 演員表 DROP TABLE IF EXISTS `actor`; CREATE TABLE `actor` ( `id` int(11) NOT NULL, `name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `update_time` datetime NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; INSERT INTO `actor` VALUES (1, 'a', '2018-10-23 16:04:40'), (2, 'b', '2018-10-23 16:04:40'), (3, 'c', '2018-10-23 16:04:40'); -- 電影表 DROP TABLE IF EXISTS `film`; CREATE TABLE `film` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_name`(`name`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; INSERT INTO `film` VALUES (1, 'film1'), (2, 'film2'), (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) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_film_actor_id`(`film_id`, `actor_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; INSERT INTO `film_actor` VALUES (1, 1, 1, NULL), (2, 1, 2, NULL), (3, 2, 1, NULL);
mysql> EXPLAIN SELECT * FROM actor;
上圖爲執行EXPLAIN展現的結果,如有join鏈接多個表時,則每join一個表多輸出一行。 其中type列是須要較長時間理解的列,固然隨着使用次數增多天然而然也會熟能生巧,因此並不須要死記硬背。
1)id編號就是select的序列號,有幾個select就有幾個id,而且id的順序是按select出現順序而增加的。 2)id越大執行優先級越高,id相同則從上至下執行,id爲null則最後執行。
select-type列表示簡單仍是複雜查詢,共有如下類型: 1)simple:簡單查詢,不包含子查詢subquery、derived和union。 2)primary:複雜查詢最外層的select。 3)subquery:select後的子查詢(不包含from後) 4)derived:from後的子查詢,MySQL會將查詢結果存入臨時表,也稱派生表。咱們經過SQL來看一下:
EXPLAIN SELECT ( SELECT 1 FROM actor WHERE id = 1 ) FROM ( SELECT * FROM film WHERE id = 1 ) der;
5)union:在union中的第二個和隨後的select。
該列表示explain的一行正在訪問哪一個表。 當from中有子查詢時,該列展現爲<derivedN>,N表示id編號,意味着先執行id=N的查詢。 當有union時,UNION RESULT的table列的值爲 <union1,2>,1和2表示參與 union 的select 行id。
這一列表示關聯類型或訪問類型,即MySQL決定如何查找表中的行,查找數據行記錄的大概範圍。 依次從最優到最差分別爲:system > const > eq_ref > ref > range > index > ALL。 通常來講,得保證查詢達到range級別,最好達到ref。 NULL:mysql可以在優化階段分解查詢語句,在執行階段用不着再訪問表或索引。 例如:在索引列中選取最小值,能夠單獨查找索引來完成,不須要在執行時訪問表 const, system: mysql能對查詢的某部分進行優化並將其轉化成一個常量(能夠看showwarnings 的結果)。 用於 primary key 或 unique key 的全部列與常數比較時,因此表最多有一個匹配行,讀取1次,速度比較快。 system是const的特例,表裏只有一條元組匹配時爲system。 eq_ref: primary key 或 unique key 索引的全部部分被鏈接使用 ,最多隻會返回一條符合條件的記錄。 這多是在 const 以外最好的聯接類型了,簡單的 select 查詢不會出現這種type。 ref:相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前綴, 索引要和某個值相比較,可能會找到多個符合條件的行。 range:範圍掃描一般出如今 in(), between ,> ,<, >= 等操做中。使用一個索引來檢索給定範圍的行。 index:掃描全表索引,這一般比ALL快一些。 ALL:即全表掃描,意味着mysql須要從頭至尾去查找所須要的行。一般狀況下這須要增長索引來進行優化了。
該列表示可能使用到的索引列。 explain時可能出現possible-keys列有值,key列爲null。可能時由於數據量較少,mysql認爲全表掃描效率更高。 若該列爲null,則沒有相關索引。此時可考慮增長適當索引來提升查詢效率。
該列表示mysql實際使用的索引。 若沒有使用索引,則該列爲null。 若是想強制mysql使用或忽視possible_keys列中的索引,在查詢中使用 force index、ignore index。
該列表示mysql使用索引的字節數,在使用聯合索引時經過key_len就能知道具體使用了那些列。 好比下面的SQL,key_len=4,就能夠推斷出僅用了聯合索引中的id列,由於int佔4個字節。
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id = 2;
key_len計算規則以下: 1)字符串 char(n):n字節長度 varchar(n):2字節存儲字符串長度,若是是utf-8,則長度3n+2 2)數值類型 tinyint:1字節 smallint:2字節 int:4字節 bigint:8字節 3)時間類型 date:3字節 timestamp:4字節 datetime:8字節 若是字段容許爲 NULL,須要1字節記錄是否爲 NULL
這一列顯示了在key列記錄的索引中,表查找值所用到的列或常量,常見的有:const(常量),字段名(例:film.id)。
這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裏的行數。
這一列展現的是額外信息。常見的重要值以下: 1)Using index:使用覆蓋索引。 2)Using where:使用 where 語句來處理結果,查詢的列未被索引覆蓋。 3)Using index condition:查詢的列不徹底被索引覆蓋,where條件中是一個前導列的範圍。 4)Using temporary:mysql須要建立一張臨時表來處理查詢。出現這種狀況通常是要進行優化的,首先是想到用索引來優化。 5)Using filesort:將用外部排序而不是索引排序,數據較小時從內存排序,不然須要在磁盤完成排序。這種狀況下通常也是要考慮使用索引來優化的。 6)Select tables optimized away:使用某些聚合函數(好比 max、min)來訪問存在索引的某個字段是。
-- 員工表 CREATE TABLE `employees` ( `id` INT ( 11 ) NOT NULL AUTO_INCREMENT, `name` VARCHAR ( 24 ) NOT NULL DEFAULT '' COMMENT '姓名', `age` INT ( 11 ) NOT NULL DEFAULT '0' COMMENT '年齡', `position` VARCHAR ( 20 ) NOT NULL DEFAULT '' COMMENT '職位', `hire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時 間', PRIMARY KEY ( `id` ), KEY `idx_name_age_position` ( `name`, `age`, `position` ) USING BTREE ) ENGINE = INNODB AUTO_INCREMENT = 4 DEFAULT CHARSET = utf8 COMMENT = '員工記錄表'; INSERT INTO employees ( NAME, age, position, hire_time ) VALUES ( 'LiLei', 22, 'manager', NOW( ) ), ( 'HanMeimei', 23, 'dev', NOW( ) ), ( 'Lucy', 23, 'dev', NOW( ) );
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;
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';
EXPLAIN SELECT * FROM employees WHERE name like '%Lei'
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';
給年齡添加單值索引。
ALTER TABLE `employees` ADD INDEX `idx_age` ( `age` ) USING BTREE;
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 100;
沒走索引緣由:mysql內部優化器會根據檢索比例、表大小等多個因素總體評估是否使用索引。 好比這個例子,多是因爲單次數據量查詢過大致使優化器最終選擇不走索引。 優化方法:能夠講大的範圍拆分紅多個小範圍。
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 50;
EXPLAIN SELECT * FROM employees WHERE age >= 51 AND age <= 100;
以上所有代碼均已在本機執行且無誤。mysql
數據結構動態演示模型sql
書寫高質量SQL的30條建議segmentfault
如有不足,敬請指正。 求知若渴,虛心若愚。