平常在CURD的過程當中,都避免不了跟數據庫打交道,大多數業務都離不開數據庫表的設計和SQL的編寫,那如何讓你編寫的SQL語句性能更優呢?php
先來總體看下MySQL邏輯架構圖:html
MySQL總體邏輯架構圖能夠分爲Server和存儲引擎層。mysql
Server層:程序員
Server層涵蓋了MySQL的大多數核心服務功能,以及全部的內置函數(如日期、時間、數學和加密函數等),以及存儲過程、觸發器、視圖等跨存儲引擎的實現也在這一層來實現。sql
存儲引擎層:數據庫
負責數據的存儲和提取,是一種插件式的架構方式。支持 InnoDB、MyISAM、Memory 等多個存儲引擎。MySQL 5.5.5版本開始默認存儲引擎是 InnoDB,也是目前經常使用的存儲引擎。segmentfault
今天咱們來看下詳細看下優化器裏的執行計劃如何分析,要分析一個 SQL 的執行效率,就要會看執行計劃,根據執行計劃優化 SQL,使其能達到高效查詢的目的。後端
一條查詢語句須要通過 MySQL 查詢優化器的各類基於成本和規則,優化後會生成一個所謂的執行計劃
。緩存
那麼這個執行計劃主要展現具體執行查詢的方式,好比多表鏈接的順序是多少,表裏包含多個索引,每一個表採用什麼訪問方法來具體執行查詢等。架構
而設計 MySQL 的大佬是很是貼心的,知道開發的朋友們都是親自寫 SQL 的,可是寫出 SQL 容易,想寫出性能高的 SQL 可不簡單。
因此,大佬提供了 Explain
語句來幫咱們查詢某個查詢語句的具體執行計劃。
本文帶你們看懂 EXPLAIN
語句,必需要熟悉各項輸出是作什麼的,從而有針對性的提高SQL 查詢語句的性能。
列名 |
用途 |
---|---|
id | 每個SELECT關鍵字查詢語句都對應一個惟一id |
select_type | SELECT關鍵字對應的查詢類型 |
table | 表名 |
partitions | 匹配的分區信息 |
types | 單表的訪問方法 |
possible_keys | 可能用到的索引 |
key | 實際使用到的索引 |
key_len | 實際使用到的索引長度 |
ref | 當使用索引列等值查詢時,與索引列進行等值匹配的對象信息 |
rows | 預估須要讀取的記錄條數 |
filtered | 某個表通過條件過濾後剩餘的記錄條數百分比 |
Extra | 額外的一些信息 |
爲了方便解釋上面的執行計劃各項輸出的含義,下面建立三張數據庫表。
DROP TABLE IF EXISTS user; CREATE TABLE `user` ( `id` int(11) NOT NULL, `name` varchar(45) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO user (`id`, `name`, `update_time`) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18'); DROP TABLE IF EXISTS `group`; CREATE TABLE `group` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `group` (`id`, `name`) VALUES (1,'group1'),(2,'group2'),(3,'group3'); DROP TABLE IF EXISTS user_group; CREATE TABLE `user_group` ( `id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `group_id` int(11) NOT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_group_id` (`group_id`), KEY `idx_user_group_id` (`user_id`,`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO user_group (`id`, `user_id`, `group_id`, `remark`) VALUES (1,1,1,'bak1'), (2,2,2,'bak2'), (3,3,3,'bak3');
下載了最新的 MySQL8.0+ 版本,直接執行 EXPLAIN
,對比了 MySQL 5.0+ 版本執行的 EXPLAIN EXTENDED
命令一樣都提供了一些查詢優化的信息。除了執行計劃各項輸出參數外,額外還有 filtered
列,是一個百分比的值,rows * filtered/100
能夠估算出將要和 EXPLAIN
中前一個表進行鏈接的行數 。
以下所示:
EXPLAIN
中的列
接下來咱們將詳細說明下 EXPLAIN
執行結果每一列的信息。
一、id 列
設計表時一般會設計 id,通常會做爲主鍵,執行計劃的結果也不例外,也有 id 列,id
列編號是 SELECT
的序列號,而且 id 的順序是按 SELECT
出現的順序增加的。id列越大執行優先級越高,id 相同則從上往下執行,id 爲 NULL 最後執行。
MySQL將 SELECT
查詢分爲簡單查詢 SIMPLE
和複雜查詢 PRIMARY
。
複雜查詢包括:簡單子查詢、派生表( FROM
語句中的子查詢)、UNION
和 UNION ALL
查詢。
簡單查詢:
複雜查詢:
1)簡單子查詢
EXPLAIN SELECT (SELECT 1 from user LIMIT 1) from user
;
2)FROM
子句中的子查詢
EXPLAIN SELECT FROM (SELECT id, count() as c from group
GROUP BY name) as derived
這個查詢執行時有個臨時表別名爲 derived
,外部 SELECT
查詢引用了這個臨時表
3)UNION
和 UNION ALL
查詢
EXPLAIN SELECT FROM user UNION SELECT FROM user;
UNION
結果老是放在一個匿名臨時表中,臨時表不在 SQL 中出現,臨時表名爲 <union1, 2>
,所以它的 id
是 NULL
,代表這個臨時表是爲了合併兩個查詢結果集而建立的。
跟 UNION
對比,UNION ALL
無需爲最終結果而去重,僅是單純的將多個查詢結果集中的記錄合併成一個並返回給用戶,因此不會使用到臨時表,故沒有 id
爲 NULL
記錄。以下所示:
EXPLAIN SELECT FROM user UNION ALL SELECT FROM user;
注意點:子查詢優化爲鏈接查詢
查詢優化器可能對子查詢進行重寫,進而轉換爲鏈接查詢
,查詢計劃中的兩個id值是相同的,以下所示:
EXPLAIN SELECT * FROM user WHERE id IN (SELECT user_id FROM user_group
);
二、select_type 列
MySQL中優化器中的概念:
物化
:
子查詢語句中的子查詢結果集中的記錄保存到臨時表的過程稱之爲 物化
(英文名:Materialize
),簡單理解爲存儲子查詢結果集的臨時表稱之爲 物化表
。
也正由於物化表的記錄都創建了索引(基於內存的物化表有哈希索引,基於磁盤的有B+樹索引),所以經過 IN
語句判斷某個操做數在不在子查詢的結果集中變得很快,從而提高語句的性能。
半鏈接 semi-join
:
也是跟 IN
語句子查詢有關。
通用語句:
SELECT ... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables ...) AND ...
outer_tables
表對 inner_tables
半鏈接的意思:
對於
outer_tables 的某條記錄來講,咱們僅關心在
inner_tables 表中是否存在匹配的記錄,而不用關心具體有多少條記錄與之匹配,最終結果只保留 outer_tables 表的記錄
。
每個 SELECT
關鍵字的查詢都定義了一個 select_type
屬性,知道這個查詢屬性就能知道在整個查詢語句中所扮演的角色。
1)SIMPLE
:簡單查詢。查詢不包含子查詢 和 UNION
。
2)PRIMARY
:複雜查詢中最外層的SELECT
,可參照上面的 UNION
查詢語句。
3)SUBQUERY
:包含的子查詢語句沒法轉換爲 semi-join
,而且爲不相關子查詢,查詢優化器採用物化方案執行該子查詢,該子查詢的第一個 SELECT
就會 SUBQUERY
。該查詢因爲被物化,只須要執行一次
。
4)DERIVED
:對於採用物化形式執行的包含派生表的查詢,該派生表的對應的子查詢爲 DERIVED
。
查詢語句以下所示:
EXPLAIN SELECT * FROM (SELECT id, count(*) as c FROM user GROUP BY id) AS derived_u where c>1;
5)UNION
:在 UNION
查詢語句中的第二個和緊隨其後的 SELECT
。
6)UNION RESULT
:MySQL選擇使用臨時表完成 UNION
查詢的去重工做。
當 select_type
爲這個值時,常常能夠看到table的值是 <unionN,M>
,這說明匹配的 id 行 是這個集合的一部分。請看上面 UNION
查詢示例。
7)MATERIALIZED
:當查詢優化器執行包含子查詢的語句時,選擇將子查詢物化以後與外層查詢進行鏈接查詢時,該子查詢類型爲 MATERIALIZED
。
8)DEPENDENT SUBQUERY
:包含的子查詢語句沒法轉換爲 semi-join
,而且爲相關子查詢,則該子查詢的第一個 SELECT
就會 DEPENDENT SUBQUERY
。該查詢可能會被執行屢次
。
8)DEPENDENT UNION
:包含的子查詢語句中包含了 UNION
或者 UNION ALL
的大查詢,這些查詢都依賴外層查詢,這些子查詢語句類型爲 DEPENDENT UNION
。
EXPLAIN SELECT * FROM user WHERE id IN (SELECT user_id FROM user_group WHERE name = 'a' UNION SELECT id FROM user WHERE name = 'b');
上面這個子查詢語句中的 SELECT user_id FROM user_group WHERE name = 'a'
這個小查詢是第一個子查詢,因此它的 select_type
爲 DEPENDENT SUBQUERY
,而 SELECT id FROM user WHERE name = 'b'
這個查詢在 UNION
後面,因此它的 select_type
爲 DEPENDENT UNION
。
最多見的值包括:SIMPLE
、PRIMARY
、DERIVED
、UNION
。
三、table 列
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可以在優化階段分解查詢語句,在執行階段用不着再訪問表或索引。例如:在索引列中選取最小值,能夠單獨查找索引來完成,不須要在執行時訪問表。
1)system,const
:MySQL 能對查詢的某部分進行優化並將其轉化成一個常量。用於主鍵或惟一二級索引列與常數比較時,因此表最多有一個匹配行,讀取1次,速度比較快
。system
是 const
的特例,表裏只有一條記錄匹配時爲 system
。
EXPLAIN SELECT FROM (SELECT FROM user where id = 1) tmp;
2)eq_ref
:在鏈接查詢時,若是被驅動表是經過主鍵或者惟一二級索引列等值匹配的方式進行訪問的,則對該被驅動表的訪問方法就是 eq_ref
。這多是在 const 以外最好的聯接類型了。
EXPLAIN SELECT * FROM user_group INNER JOIN user ON user_group.user_id = user.id;
3)ref
:相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前綴,索引要和某個值相比較,可能會找到多個符合條件的行。
a. 簡單 SELECT
查詢,name 是普通索引(非惟一索引)。
EXPLAIN SELECT * FROM user where user.name = 'a';
b. 關聯表
查詢,idx_user_group_id (user_id,group_id)
爲聯合索引,這裏使用到了user_group聯合索引最左邊前綴 user_id。
EXPLAIN SELECT user_id FROM user LEFT JOIN user_group ON user.id = user_group.user_id;
4)ref_or_null
:對普通二級索引進行等值查詢,該索引列也能夠爲NULL值時。
EXPLAIN SELECT * FROM user where user.name = 'a' OR name IS NULL;
5)index_merge
:MySQL使用索引合併的方式執行的。
EXPLAIN SELECT * FROM user WHERE user.name = 'a' OR user.id = 1;
6)range
:使用索引獲取範圍區間
的記錄,一般出如今 in, between ,> ,<, >=
等操做中。
EXPLAIN SELECT * FROM user WHERE user.id > 1;
7)index
:掃描全表索引,這一般比ALL快一些。(index
是從索引中讀取的,而 ALL
是從硬盤中讀取)
group
表裏的兩個字段都有索引。
EXPLAIN SELECT * FROM group
;
8)ALL
:即全表掃描,MySQL 須要從頭至尾去查找表中所須要的行。一般狀況下這須要增長索引來進行優化了。
EXPLAIN SELECT * FROM user
;
五、possible_keys 列
possible_keys
列表示查詢可能使用哪些索引來查找。
EXPLAIN
執行計劃結果可能出現 possible_keys
列,而 key
顯示 NULL
的狀況,這種狀況是由於表中數據很少,MySQL 會認爲索引對此查詢幫助不大,選擇了全表查詢。
若是 possible_keys
列爲 NULL
,則沒有相關的索引。在這種狀況下,能夠經過檢查 WHERE
子句去分析下,看看是否能夠創造一個適當的索引來提升查詢性能,而後用 EXPLAIN
查看效果。
另外注意:不是這一列的值越多越好,使用索引過多,查詢優化器計算時查詢成本高,因此若是可能的話,儘可能刪除那些不用的索引。
六、key 列
key
列表示實際採用哪一個索引來優化對該表的訪問。
若是沒有使用索引,則該列是 NULL。若是想強制 MySQL使用或忽視 possible_keys
列中的索引,在查詢中使用 force index
、ignore index
。
七、key_len 列
key_len
列表示當查詢優化器決定使用某一個索引查詢時,該索引記錄的最大長度。
key_len
列計算規則以下:
char(n):n字節長度
varchar(n):2字節存儲字符串長度,若是是utf-8,則長度 3n + 2
注意:該索引列能夠存儲NULL
值,則key_len
比不能夠存儲NULL
值時多1個字節。
好比:varchar(50),則實際佔用的key_len
長度是 3 * 50 + 2 = 152,若是該列容許存儲NULL
,則key_len
長度是153。
tinyint:1字節
smallint:2字節
int:4字節
bigint:8字節
date:3字節
timestamp:4字節
datetime:8字節
索引最大長度是768字節,當字符串過長時,MySQL 會作一個相似左前綴索引的處理,將前半部分的字符提取出來作索引。
舉例1:
user_group
表中的聯合索引 idx_user_group_id
由 user_id
和 group_id
兩個int 列組成,而且每一個 int 是 4 字節。
EXPLAIN SELECT * FROM user_group WHERE user_id = 2;
經過結果中的 key_len=4可推斷出查詢使用了第一個列:user_id
列來執行索引查找。
舉例2:
再看 user
表 name 字段是 varchar(45) 變長字符串類型,key_len
爲138 等於 45 * 3 + 2 (變長字節) + 1字節(容許存儲NULL值)
EXPLAIN SELECT * FROM user WHERE name = 'a';
因此,之後再看到 key_len
字段的值,不要在懵逼咯,固定套路~
八、ref 列
ref
列顯示了在 key
列記錄的索引中,表查找值所用到的列或常量,常見的有:const
(常量),字段名
(例:user.id
)。
九、rows 列
rows
列是查詢優化器估計要讀取並檢測的行數,注意這個不是結果集裏的行數。
若是查詢優化器使用全表掃描查詢,rows
列表明預計的須要掃碼的行數;
若是查詢優化器使用索引執行查詢,rows
列表明預計掃描的索引記錄行數。
十、filtered 列
對於單表來講意義不大,主要用於鏈接查詢中。
前文中也已提到 filtered
列,是一個百分比的值,對於鏈接查詢來講,主要看驅動表
的 filtered
列的值 ,經過 rows * filtered/100
計算能夠估算出被驅動表
還須要執行的查詢次數。
EXPLAIN SELECT * FROM user INNER JOIN user_group ON user.id = user_group.user_id WHERE user.update_time = '2019-01-01';
能夠看到驅動表user
執行的rows列爲3行,filtered列爲 33.33,計算驅動表的扇出值
爲 3 * 33.33% 約等於1,說明還須要對被驅動表執行大約1次查詢。
十一、Extra 列
Extra
列提供了一些額外信息。這一列在 MySQL中提供的信息有幾十個,這裏僅列舉一些常見的重要值以下:
1)Using index
:查詢的列被索引覆蓋,而且 WHERE
篩選條件是索引的前導列,使用了索引性能高。通常是使用了覆蓋索引(查詢列都是索引列字段)。對於 INNODB 存儲引擎來講,若是是輔助索引性能會有很多提升,而且也不須要回表查詢。
2)Using where Using index
:查詢的列被索引覆蓋,而且 WHERE
篩選條件是索引列之一,但並非索引的前導列,意味着沒法直接經過索引查找來查詢到符合條件的數據。
3)NULL
:查詢的列未被索引覆蓋,而且 WHERE
篩選條件是索引的前導列,意味着用到了索引,可是部分字段未被索引覆蓋,必須經過 回表
來查詢,不是純粹地用到了索引,也不是徹底沒用到索引。
4)Using index condition
:與Using where
相似,查詢的列不徹底被索引覆蓋,WHERE
條件中是一個前導列的範圍。
5)Using temporary
:MySQL 中須要建立一張內部臨時表來處理查詢,通常出現這種狀況就須要考慮進行優化了,首先是想到用索引來優化。
一般在許多執行包括DISTINCT、GROUP BY、ORDER BY等子句查詢過程當中,若是不能有效利用索引來完成查詢,MySQL頗有可能會尋求創建內部臨時表來執行查詢。
因此,執行計劃中出現了 Using temporary
並非個好兆頭,由於創建與維護臨時表要付出很大的成本的,要考慮使用索引
來優化改進。
6)Using filesort
:MySQL 會對結果使用一個外部索引排序,而不是按索引次序從表裏讀取行。此時 MySQL 會根據聯接類型瀏覽全部符合條件的記錄,並保存排序關鍵字和行指針,而後排序關鍵字並按順序檢索行信息。這種狀況下通常也是要考慮使用索引來優化的。
查詢中須要使用 filesort
的方式進行排序的記錄很是多,那麼這個過長是很耗時的,想辦法將使用 文件排序
的執行方式改進爲使用索引
進行排序。
7)Index merges
:一般顯示爲Using sort_union(...)
說明準備用 Sort-Union
索引合併方式來查詢;顯示爲 Using union(...)
,說明準備用Union
索引合併方式查詢;顯示爲Using intersect(...)
,說明準備使用Intersect
索引合併方式查詢。
8)LooseScan
:在 IN 子查詢轉爲 semi-join
時,若是採用的是 LooseScan
執行策略,則會在Extra
中提示。
9)FirstMatch(tbl_name)
:在 IN 子查詢轉爲 semi-join
時,若是採用的是 FirstMatch
執行策略,則會在Extra
中提示。
10)Using join buffer
:強調了在獲取鏈接條件時沒有使用索引,而且須要鏈接緩衝區來存儲中間結果。出現該值,應該注意,根據查詢的具體狀況可能須要添加索引來改進性能。
咱們所提到的回表
操做 ,實際上是一種隨機IO,比較耗時,因此儘可能避免上面提到的回表操做,當發現Extra
提示爲 Using filesort
、Using temporary
時就須要格外注意了,考慮索引優化。
staff
表表演使用:# 重建 `staff` 表 DROP TABLE `staff`; CREATE TABLE `staff` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名', `s_name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '花名', `s_no` INT(4) NOT NULL DEFAULT 0 COMMENT '工號', `work_age` int(11) NOT NULL DEFAULT '0' COMMENT '工齡', `position` varchar(20) NOT NULL DEFAULT '' COMMENT '職位', `arrival_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時間', `remark` VARCHAR(500) DEFAULT NULL COMMENT '備註', # 容許 NULL PRIMARY KEY (`id`), # 主鍵 UNIQUE KEY idx_s_name (s_name), # 惟一索引 KEY idx_s_no (s_no), # 普通索引 KEY `idx_name_age_position` (`name`,`work_age`,`position`) USING BTREE # 聯合索引 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='員工記錄表'; # 初始化 `staff` 表數據 INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangsan','zs',10,2,'manager',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('lisi','ls',11,3,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('wangwu','ww',12,8,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangliu','zl',110,5,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('xiaosun','xs',111,5,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('donggua','dg',200,3,'dev',NOW());
一、全值匹配:
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan';
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 2;
EXPLAIN SELECT * FROM staff where name = 'zhangsan' AND work_age = 2 AND position = 'dev';
EXPLAIN SELECT * FROM staff where position = 'dev' AND name = 'zhangsan' AND work_age = 2;
最後一條,咱們將 position
放到了 WHERE
條件後面,儘管沒有按照聯合索引的順序編寫條件,MySQL 優化器會自動優化,將 name 排到最前面去,因此仍是會正確使用聯合索引的。
聯合索引建立後,你必須嚴格按照最左前綴的原理進行使用,不然會沒法使用到索引。儘可能按照這個順序去寫,這樣避免 MySQL 優化器再次優化了。
二、最佳左前綴法則:
若是索引了多列,要遵照最左前綴法則。指的是查詢從索引的最左前列開始而且不跳過索引中的列。
如下 SQL 符合最左前綴匹配法則:
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan' AND work_age = 3 AND position = 'manager';
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan' AND position = 'manager';
如下執行都是全表掃描,type
爲 ALL
,都不符合最左前綴法則:
EXPLAIN SELECT * FROM staff WHERE work_age = 2 AND position ='dev';
EXPLAIN SELECT * FROM staff WHERE position = 'dev';
三、索引列上避免作計算操做
索引上儘可能避免作函數計算等操做,會致使索引失效而轉向全表掃描。
WHERE
條件後面索引列使用函數:
EXPLAIN SELECT * FROM staff WHERE LEFT(name, 5) = 'zhang';
EXPLAIN SELECT * FROM staff WHERE LOWER(name) = 'zhangsan';
EXPLAIN SELECT FROM staff WHERE staff.s_no 2 > 3;
查詢的結果 type 列爲 ALL
,key 是空的,索引失效,全表掃描。
計算邏輯儘可能放到業務層去處理,最大限度的命中索引,同時還能節省數據庫資源開銷。
四、存儲引擎沒法使用索引中範圍條件右邊的列
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age > 2 AND position ='dev';
咱們看到了執行結果中 type 爲 range
級別,使用了範圍查找,而 position 字段並無用到索引(沒有使用到BTree的索引去查詢),只是從 name = 'zhangsan' AND work_age > 2
條件返回的結果集中,再過濾符合 position 字段條件的數據。
五、儘可能使用覆蓋索引
覆蓋索引:簡單理解,只訪問建了索引的列。減小使用 SELECT *
語句查詢列。
使用了覆蓋索引:
EXPLAIN SELECT name,work_age FROM staff WHERE name= 'zhangsan' AND work_age = 3;
使用了 SELECT *
查詢:
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 3;
咱們重點看下使用了 覆蓋索引
方式查詢,會在結果中 Extra
列顯示 Using index
,這說明在查詢列包含了索引列,不須要再次回表查詢了。而若是使用 SELECT *
方式查詢,查詢列包含非索引的列,Extra
顯示爲 NULL
,因此還會進行回表查詢。
附一個曾經線上SQL的優化記錄:
artist 表有幾十萬條的數據量,第一條執行的SQL沒有索引直接查詢,查詢耗時 0.557
毫秒;第一次優化
新建 founded 字段做爲普通索引,查詢耗時 0.0224
毫秒;第二次優化
再次重建聯合索引 founded_name,優化後查詢耗時:0.0051
毫秒。由於使用了覆蓋索引查詢方式,基於此優化,SQL查詢效率提高很是明顯。
六、範圍條件查找可以命中索引
範圍條件主要包括 <、<=、>、>=、between
等。
若條件中範圍列有普通索引和主鍵索引同時存在, 優先使用主鍵索引:
EXPLAIN SELECT * FROM staff WHERE staff.s_no > 10 AND staff.id > 2;
範圍列能夠用到索引,注意聯合索引必須符合最左前綴法則,若是查詢條件中有兩個範圍列則沒法全用到索引,優化器會去選擇:
EXPLAIN SELECT * FROM staff WHERE staff.name != 'zl' AND staff.s_no > 1;
若條件中範圍查詢和等值查詢同時存在,優先匹配等值查詢列的索引:
EXPLAIN SELECT * FROM staff WHERE staff.s_no > 10 AND staff.s_name = 'zl';
七、索引列不爲 NULL,IS NOT NULL沒法使用索引
索引列建議都使用 NOT NULL 約束
及默認值,單列索引不存 NULL 值,聯合索引不存所有爲 NULL 的值,若是列容許爲 NULL,查詢結果可能不符合預期。
staff 表中爲 remark
字段新建普通索引:
ALTER TABLE staff ADD INDEX idx_remark (remark);
IS NULL
查詢命中索引:
EXPLAIN SELECT * FROM staff WHERE staff.remark IS NULL;
IS NOT NULL
查詢不會命中索引:
EXPLAIN SELECT * FROM staff WHERE staff.name IS NOT NULL;
八、模糊條件查詢以通配符開頭索引失效
like '%xx'
或 like '%xx%'
前導模糊查詢不能命中索引:
EXPLAIN SELECT * from staff where name like '%zhang%';
如何使用模擬查詢才能命中索引?
a)like 'xx%'
非前導模糊查詢能夠命中索引:
EXPLAIN SELECT * FROM staff WHERE name LIKE 'zhang%';
b)使用覆蓋索引,查詢字段必需要創建覆蓋索引字段
EXPLAIN SELECT name,work_age FROM staff WHERE name LIKE '%zhang%';
聯合索引是 idx_name_work_age_position
九、字符串類型不加單引號索引失效
字符串的數據類型必定要將常量值使用單引號,這個在平常開發中要特別注意的,數據類型出現隱式轉換的時候不會命中索引。
不加單引號索引失效
EXPLAIN SELECT * FROM staff WHERE name = 1;
name=1 相似於在該字段上作了一個函數運算,所以不會走索引的。
加單引號會命中索引:
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan';
十、OR
使用多數狀況下索引會失效
EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR work_age = 2;
儘管 name 和 work_age 是聯合索引,可是 work_age 列上並無建索引,因此使用了 OR
不會走索引。
若是 OR
先後都是聯合索引帶頭大哥 name 字段,那麼就會用到索引,以下所示:
因 OR
後面的條件列中沒有索引,會走全表掃描。存在全表掃描的狀況下,就沒有必要多一次索引掃描增長IO訪問。
可以使用覆蓋索引查詢:
EXPLAIN SELECT name,work_age FROM staff WHERE name='zhangsan' OR work_age = 2;
OR 後面也使用索引列:
EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR s_name='wangwu';
s_name 是惟一索引,name是聯合索引第一個字段,二者使用 OR
查詢結果 Extra
顯示 Using sort_union(idx_name_age_position,idx_s_name); Using where
解釋一下。
若是執行計劃 Extra
列出現了 Using sort_union(...)
的提示,說明準備使用 Sort-Union
索引合併的方式執行查詢。若是出現了 Using intersect(...)
的提示,說明準備使用 Intersect
索引合併方式執行查詢,若是出現了 Using union(...)
的提示 ,說明準備使用 Union
索引合併方式執行查詢。 括號中 ...
表示須要進行索引合併的索引名稱。
使用UNION優化改進:
EXPLAIN SELECT FROM staff WHERE name='zhangsan' UNION SELECT FROM staff WHERE s_name = 'zs';
使用 UNION
執行計劃中出現了第三條記錄,Extra
中出現 Using temporary
,說明 MySQL由於不能有效利用索引,創建了內部臨時表來執行查詢。當你在使用 DISTINCT 、GROUP BY、UNION
等子句中的查詢過程當中,都有可能會出現該擴展信息。
使用UNION ALL進一步優化:
EXPLAIN SELECT FROM staff WHERE name='zhangsan' UNION ALL SELECT FROM staff WHERE s_name = 'zs';
執行結果中再也不出現內部臨時表,具體用的時候結合實際需求來定是否使用。
十一、負向查詢條件不能使用索引,能夠優化爲 IN
查詢
負向查詢條件包括:!=、<>、NOT IN、NOT EXISTS、NOT LIKE
等。
不會命中索引:
EXPLAIN SELECT * FROM staff WHERE s_no !=1 AND s_no != 2;
EXPLAIN SELECT * FROM staff WHERE s_no NOT IN (1,2);
使用IN優化,命中索引:
EXPLAIN SELECT * FROM staff WHERE s_no IN (11,12);
可是使用 IN
命中索引有個前提,是查詢條件字段數據區分度要高,一般如:狀態、類型、性別之類的字段。
十二、排序對索引的影響
ORDER BY
是常常用的語句,排序也遵循最左前綴列的原則。
查詢全部列未命中索引:
EXPLAIN SELECT * FROM staff ORDER BY name,work_age;
覆蓋索引查詢可命中索引:
覆蓋索引可以利用聯合索引查詢,可是 ORDER BY
後的條件查詢不符合最左前綴原則,執行結果 Extra
中出現了 Using filesort
的提示,通常看到這個就要想辦法優化了。
調整排序的兩個字段順序以後,Extra
會提示爲 Using index
,使用了索引,避免了排序的資源開銷:
EXPLAIN SELECT name,work_age FROM staff ORDER BY name,work_age;
1三、局部索引的使用
局部索引,區別於最左列索引(順序取索引中靠左的列的查詢),它只取某列的一部分做爲索引。
INNODB存儲引擎下,通常是字符串類型,很長,所有做爲索引大大增長存儲空間,索引也須要維護,對於長字符串,又想做爲索引列,可取的辦法就是取前一部分(局部),表明一整列做爲索引串。
如何確保這個前綴能表明或大體表明這一列?MySQL中有個概念是 索引選擇性
,是指索引中不重複的值的數目(也稱基數X)與整個表該列記錄總數(T)的比值。基數能夠經過SHOW INDEX FROM 表名
查看。
好比一個列表 [1,2,2,3,5,6],總數是 6,不重複值數目爲 5,選擇性爲 5/6,所以選擇性範圍是[X/T, 1],這個值越大,表示列中不重複值越多,越適合做爲局部索引,而惟一索引(UNIQUE KEY)的選擇性是1。
`SELECT COUNT(DISTINCT(CONCAT(LEFT(remark, N))/COUNT(*) FROM t; 測試出接近 1 的索引選擇性,其中N是索引的長度,窮舉法去找出N的值,而後再建索引。
建立 局部索引
,使用 remark 字段舉個例子
EXPLAIN SELECT * FROM staff where remark LIKE 'xxx%';
對 remark 字段重建局部索引:
ALTER TABLE staff DROP INDEX idx_remark_part, ADD INDEX idx_remark_part(remark(5));
再次執行查詢:
EXPLAIN SELECT * FROM staff where remark LIKE 'xxx%';
上面列了大部分場景索引最佳實戰,除此以外,不宜建索引的幾點小總結:
1)更新很是頻繁字段不宜建索引
由於字段更新臺頻繁,會致使B+樹的頻繁的變動,重建索引。因此這個過程是十分消耗數據庫性能的。
2)區分度不大的字段不宜建索引
好比相似性別這類的字段,區分度不大,創建索引的意義不大。由於不能有效過濾數據,性能和全表掃描至關。另外注意一點,返回數據的比例在 30%
以外的,優化器不會選擇使用索引。
3)業務中有惟一特性的字段,建議建成惟一索引
業務中若是有惟一特性的字段,即便是多個字段的組合,也儘可能都建成惟一索引。儘管惟一索引會影響插入效率,可是對於查詢的速度提高是很是明顯的。此外,還可以提供校驗機制,若是沒有惟一索引,高併發場景下,可能還會產生髒數據。
4)多表關聯時,要確保關聯字段上必須有索引
5)建立索引時避免創建錯誤的認識
索引越多越好,認爲一個查詢就須要建一個索引。
寧缺勿濫,認爲索引會消耗空間、嚴重拖慢更新和新增速度。
抵制惟一索引,認爲業務的惟一性一概須要在應用層經過「先查後插」方式解決。
過早優化,在不瞭解系統的狀況下就開始優化。
6)最佳索引實踐口訣
若是你以爲上面哪些太囉嗦,有朋友已總結爲一套優化口訣,優化SQL時也能提個醒吧。
全值匹配我最愛,最左前綴要遵照;
帶頭大哥不能死,中間兄弟不能斷;
索引列上少計算,範圍以後全失效;
Like百分寫最右,覆蓋索引不寫星;
不等空值還有or,索引失效要少用;
VAR引號不可丟,SQL高級也不難!
7)EXPLAIN
執行計劃實踐總結
若是仍是以爲 EXPLAIN
執行計劃列太多了,也記不住呀,那麼請重點關注如下幾列:
第1列
:ID越大,執行的優先級越高;ID相等,從上往下優先順序執行。
第2列
:select_type 查詢語句的類型,SIMPLE簡單查詢,PRIMARY複雜查詢,DERIVED衍生查詢(from子查詢的臨時表),派生表。
第4列
:請重點掌握,type類型,查詢效率優先級:system->const->eq_ref->ref->range->index->ALL
ALL
是最差
的,system
是最好
的,性能最佳,阿里巴巴開發規約中要求最差也獲得 range
級別,而不能有 index、ALL
。
最後,對於後端工程師而言,盡力都能掌握 EXPLAIN
的使用,寫完SQL請習慣性的用它幫助你分析一下,作一個對SQL性能有追求的程序員,由於SQL也是程序員必備技能,將慢查詢問題拍死在項目上線前夕。
若是以爲本文有所收穫,歡迎轉發分享。
參考資料:
MySQL官網
https://www.cnblogs.com/songw...
https://www.cnblogs.com/phpdr...
歡迎關注個人公衆號,掃二維碼關注得到更多精彩原創文章,與你一同成長~