MO_or關於SQL優化的感悟

1、引言

本文是對SQL優化的複習總結,主要記錄如何使用索引優化SQL,數據庫爲MySQL。主要從三個部分依次進行探討。
第一部分:理解MySQL索引底層數據結構。
第二部分:SQL分析工具Explain詳解。
第三部分:MySQL的索引最佳實踐。

強烈建議:因爲本文篇幅較長,內容較多。推薦讀者每次僅閱讀一部分,請勿一次性讀完(並不利於消化吸取,大佬除外)。html

2、MySQL索引

2.1 索引的簡單介紹

索引就是一種幫助MySQL高效獲取數據的排好序的數據結構。

2.2 索引的數據結構

這裏先說結論,MySQL索引的數據結構是B+TREE。

咱們再依次從二叉樹到B+TREE,逐步的理解MySQL索引爲何使用B+TREE。

在開始討論具體的數據結構以前,咱們應該先初步的瞭解什麼是數據結構,數據結構的做用是什麼?
若是你們瞭解設計模式,或者看過個人《MO_or的單例模式複習總結》就能知道。
設計模式是提供了針對不一樣類型的問題的優質解決方案。
相對應的,數據結構其實就是提供了針對不一樣數據的優質存儲方案,
固然這些方案同設計模式同樣,也是經過前輩們無數次的實踐試錯改良所得出的。
那麼下面就正式進入幾種數據結構的講解。

2.2.1 二叉樹

在瞭解二叉樹以前,咱們須要先明白索引爲何要用數據結構?
結合下圖,假如咱們在不使用數據結構(圖片左側)狀況下,須要讀取Col2=89,那麼磁盤就須要進行6次I/O。
若是使用二叉樹來存儲數據(圖片右側),那磁盤僅需進行2此I/O,這就顯著的減小了I/O次數,其效率也就相應獲得了提高。
由此咱們就能明白爲何索引須要使用數據結構。那索引爲何不使用二叉樹,而要使用B+TREE呢?

二叉樹.png

上圖的右側即是一個常見的二叉樹模型(模型演示的網址在5、參考)。
二叉樹的規則爲下一節點左邊的元素小於上一節點元素(22<34),
下一節點右邊的元素大於等於上一節點元素(89>=34)。
結合下圖,咱們就能明白爲何索引不使用二叉樹。

極端狀況二叉樹.png

從上圖中便能看出,當元素都爲依次遞增的狀況下,二叉樹的元素節點則變成了按單列的方式排布。
這時咱們若想取元素6時,那麼也只能讓磁盤進行6次I/O。
因而爲了解決這個問題,咱們就可使用紅黑樹(平衡二叉樹)。

2.2.2 紅黑樹(平衡二叉樹)

直接上圖,一樣是元素依次遞增的狀況下。

紅黑樹.png

能夠看出紅黑樹在基於二叉樹的基礎上,進行了平衡,再也不是以單列的方式進行排布。
但索引爲何依然沒有使用紅黑樹?由於目前數據量較小,層級不高,但數據庫中一般會出現幾十萬、幾百萬乃至上千萬的數據。
咱們能夠估算下,若表中存儲的數據有100萬條,既2^n=100萬,n=log(2)(100萬),n≈20。
這意味着若咱們須要取得數在最深的節點上,那麼就須要讀寫20次及以上的I/O。
由此咱們能夠看出,紅黑樹在遇到大數據量時,性能依舊較差。並不符合索引能夠高效獲取數據的這一特色。

2.2.3 B-TREE(多路搜索樹)

爲了解決紅黑樹在存儲大量數據的狀況下,層級依舊很深的問題。因而就有了更好方案,B-TREE。那麼咱們先看下B-TREE的模型吧。

B-TREE.png

從上圖能夠直觀的看出,在紅黑樹的基礎上。B-TREE的葉節點(1五、5六、77所相似的行),從原來只能存儲一個元素,變爲了能夠存儲多個元素。
這樣就大大增長了每一個葉節點的利用空間,減小了層數。但咱們也看到每一個數字節點下方還有個data。
那麼新的問題便產生了,這個data即爲所存儲的數據,那麼當一行數據過大時,每一個葉節點所能容納的元素就相應減小了。
咱們能夠經過如下SQL,來查詢葉節點的大小,一般爲16kb,
SHOW GLOBAL STATUS LIKE 'INNODB_page_size';
若假設一個data爲1kb,那意味着每一個葉節點最多能容納16個元素。那麼當數據量過多時上千萬,依然存在紅黑樹同樣的問題。

2.2.4 B+TREE

那麼終於輪到B+TREE上場了,咱們經過下圖一塊兒看看B+TREE是如何巧妙地解決B-TREE所面臨的問題的吧。

B+TREE.png

能夠看出,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的緣由。

2.3 最左前綴原理

這裏僅簡單歸納其原理,具體如何在SQL中體現的,將結合3、Explain的部分進行解讀。
當使用聯合索引(由多個列組成的索引)時,查詢需聽從從左到右的順序,且不能跳過中間的列。不然會致使索引失效。

3、Explain

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

image.png

上圖爲執行EXPLAIN展現的結果,如有join鏈接多個表時,則每join一個表多輸出一行。
其中type列是須要較長時間理解的列,固然隨着使用次數增多天然而然也會熟能生巧,因此並不須要死記硬背。

3.1.1 id列

1)id編號就是select的序列號,有幾個select就有幾個id,而且id的順序是按select出現順序而增加的。
2)id越大執行優先級越高,id相同則從上至下執行,id爲null則最後執行。

3.1.2 select-type列

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;

image.png

5)union:在union中的第二個和隨後的select。

3.1.3 table列

該列表示explain的一行正在訪問哪一個表。
當from中有子查詢時,該列展現爲<derivedN>,N表示id編號,意味着先執行id=N的查詢。
當有union時,UNION RESULT的table列的值爲 <union1,2>,1和2表示參與 union 的select 行id。

3.1.4 type列

這一列表示關聯類型或訪問類型,即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須要從頭至尾去查找所須要的行。一般狀況下這須要增長索引來進行優化了。

3.1.5 possible_keys列

該列表示可能使用到的索引列。
explain時可能出現possible-keys列有值,key列爲null。可能時由於數據量較少,mysql認爲全表掃描效率更高。
若該列爲null,則沒有相關索引。此時可考慮增長適當索引來提升查詢效率。

3.1.6 key列

該列表示mysql實際使用的索引。
若沒有使用索引,則該列爲null。
若是想強制mysql使用或忽視possible_keys列中的索引,在查詢中使用 force index、ignore index。

3.1.7 key_len列

該列表示mysql使用索引的字節數,在使用聯合索引時經過key_len就能知道具體使用了那些列。
好比下面的SQL,key_len=4,就能夠推斷出僅用了聯合索引中的id列,由於int佔4個字節。
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id = 2;

image.png

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

3.1.8 ref列

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

3.1.9 rows列

這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裏的行數。

3.1.10 Extra列

這一列展現的是額外信息。常見的重要值以下:
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)來訪問存在索引的某個字段是。

4、索引最佳實踐

-- 員工表
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( ) );

4.1全值匹配

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';

image.png

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;

image.png

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';

image.png

4.2.最左前綴法則

若是索引了多列,要遵照最左前綴法則。指的是查詢從索引的最左前列開始而且不跳過索引中的列。

4.3.不在索引列上作任何操做(計算、函數、(自動or手動)類型轉換),會致使索引失效而轉向全表掃描

4.4.存儲引擎不能使用索引中範圍條件右邊的列

EXPLAIN SELECT \* FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';

image.png

4.5.儘可能使用覆蓋索引(只訪問索引的查詢(索引列包含查詢列)),減小select *語句

4.6.mysql在使用不等於(!=或者<>)的時候沒法使用索引會致使全表掃描

4.7.is null,is not null 也沒法使用索引

4.8.like以通配符開頭('$abc...')mysql索引失效會變成全表掃描操做

EXPLAIN SELECT * FROM employees WHERE name like '%Lei'

image.png

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

4.10.少用or或in,用它查詢時,mysql不必定使用索引,mysql內部優化器會根據檢索比例、表大小等多個因素總體評估是否使用索引,詳見範圍查詢優化

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';

image.png

4.11.範圍查詢優化

給年齡添加單值索引。
ALTER TABLE `employees` ADD INDEX `idx_age` ( `age` ) USING BTREE;
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 100;

image.png

沒走索引緣由:mysql內部優化器會根據檢索比例、表大小等多個因素總體評估是否使用索引。
好比這個例子,多是因爲單次數據量查詢過大致使優化器最終選擇不走索引。
優化方法:能夠講大的範圍拆分紅多個小範圍。
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 50;
EXPLAIN SELECT * FROM employees WHERE age >= 51 AND age <= 100;

image.png


以上所有代碼均已在本機執行且無誤。mysql

5、參考

數據結構動態演示模型sql

MO_or的單例模式複習總結數據庫

書寫高質量SQL的30條建議segmentfault

6、最後

如有不足,敬請指正。
求知若渴,虛心若愚。
相關文章
相關標籤/搜索