4. 性能分析
4.1 MySQL 常見性能瓶頸
① CPU:CPU 在滿負荷運行一般發生在數據裝入到內存或從磁盤讀取數據時;
② IO:磁盤 IO 瓶頸發生在裝入數據遠大於內存容量時;
③ 服務器硬件瓶頸:通過 top、free、iostat、vmstat
命令查看系統的性能和狀態;
④ 數據庫服務器配置問題;
4.2 MySQL 性能分析
4.2.1 EXPLAIN 簡介
使用 EXPLAIN
關鍵字可以模擬 MySQL 優化器執行 SQL 查詢語句,從而查看 MySQL 是如何理解你的 SQL 語句,用於分析 SQL 查詢語句或者表結構的性能瓶頸。
類似於醫院檢查的化驗報告單。
作用:
① 獲取表的讀取順序;
② 獲得數據讀取操作的操作類型;
③ 查看那些索引可以使用;
④ 查看哪些索引實際被使用;
⑤ 查看錶之間的引用關係;
⑥ 查看每張表有多少行被優化器查詢;
4.2.2 使用方法:
EXPLAIN + SQL 語句
示例:
mysql> explain select * from tbl_emp where id = 2;
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | tbl_emp | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
EXPLAIN
執行後返回的信息列:
id
:查詢的序列號,包含一組數字,表示查詢中執行SELECT
子句或操作表的順序;select_type
:表示查詢的類型;table
:表示數據所在的表;type
:查詢的訪問類型;possible_keys
:顯示可能在這張表中的索引,一個或多個;key
:實際使用到的索引;key_len
:表示索引中使用的字節數,可以通過該列計算查詢中使用的索引長度ref
:顯示索引的那一列被使用;rows
:顯示 MySQL 認爲執行查詢時必須檢查的行數;Extra
:其他的額外重要的信息;
4.2.2 各分析字段解釋
使用以下 SQL 語句創建數據表
CREATE TABLE t1(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t2(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t3(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t4(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));
INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));
INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));
4.2.2.1 id
查詢的序列號,包含一組數字,表示查詢中執行 SELECT
子句或操作表的順序;
主要分爲三種情況:
① id 相同,執行順序由上至下;
mysql> explain select * from t1,t2,t3 where t1.id = t2.id and t2.id = t3.id;
+----+-------------+-------+--------+---------------+---------+---------+-----------------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+------+-------+
| 1 | SIMPLE | t1 | ALL | PRIMARY | NULL | NULL | NULL | 1 | |
| 1 | SIMPLE | t2 | eq_ref | PRIMARY | PRIMARY | 4 | base_crud.t1.id | 1 | |
| 1 | SIMPLE | t3 | eq_ref | PRIMARY | PRIMARY | 4 | base_crud.t1.id | 1 | |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+------+-------+
3 rows in set (0.00 sec)
② id 不同,如果是子查詢,id 的序號會遞增,id 值越大優先級越高,越先被執行;
mysql> explain select t1.id from t1 where t1.id in (select t2.id from t2 where t2.id in (select t3.id from t3 where t3.content = ''));
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
| 1 | PRIMARY | t1 | index | NULL | PRIMARY | 4 | NULL | 1 | Using where; Using index |
| 2 | DEPENDENT SUBQUERY | t2 | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | Using index; Using where |
| 3 | DEPENDENT SUBQUERY | t3 | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | Using where |
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
3 rows in set (0.00 sec)
③ id 有相同的也有不同的,id 如果相同,可以認爲是一組的,從上往下執行;所在組中 id 值越大,優先級越高
mysql> explain select t2.* from t2, (select t3.* from t3) s3 where s3.id = t2.id;
+----+-------------+------------+--------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+---------------+---------+---------+-------+------+-------+
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
| 1 | PRIMARY | t2 | const | PRIMARY | PRIMARY | 4 | const | 1 | |
| 2 | DERIVED | t3 | ALL | NULL | NULL | NULL | NULL | 1 | |
+----+-------------+------------+--------+---------------+---------+---------+-------+------+-------+
3 rows in set (0.00 sec)
總結:每個 id 號碼。表示一個獨立的查詢,SQL 的查詢行數越少越好
4.2.2.2 select_type
表示查詢的類型,主要用於區分普通查詢、聯合查詢、子查詢等複雜查詢。
select_type屬性 | 含義 |
---|---|
SIMPLE |
簡單的 SELECT 查詢,查詢中不包含子查詢或者 UNION |
PRIMARY |
查詢中若包含複雜查詢的子部分,最外層查詢則標記爲 PAIMARY |
DERIVED |
在 FROM 列表中包含的子查詢被標記爲 DERIVED (衍生)MySQL 會遞歸執行這些子查詢,把結果放到臨時表中。 |
SUBQUERY |
在 SELECT 或 WHERE 列表中包含了子查詢。 |
DEPEDENT SUBQUERY |
在 SELECT 或 WHERE 列表中包含了子查詢,子查詢基於外層。 |
UNCACHEABLE SUBQUERY |
無法使用緩存的子查詢。 |
UNION |
若第二個 SELECT 出現在 UNION 之後,則被標記爲 UNION ;若 UNION 包含在 FROM 子句的子查詢中,外層 SELECT 將被標記爲 DERIVED |
UNION RESULT |
從 UNION 表獲取結果的 SELECT |
① SIMPLE
:表示單表查詢
mysql> explain select * from t1;
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | t1 | ALL | NULL | NULL | NULL | NULL | 1 | |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
1 row in set (0.00 sec)
② PRIMARY
:查詢中 若包含任何複雜的子部分,最外層查詢則會被標記爲 PRIMARY
mysql> explain select * from (select * from t2) a;
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
| 2 | DERIVED | t2 | ALL | NULL | NULL | NULL | NULL | 1 | |
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
2 rows in set (0.00 sec)
③ DERIVED
:在 FROM
列表中包含的子查詢被標記爲 DERIVED
(衍生)MySQL 會遞歸執行這些子查詢,把結果放大臨時表中。
mysql> explain select * from (select * from t2) a;
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
| 2 | DERIVED | t2 | ALL | NULL | NULL | NULL | NULL | 1 | |
+----+-------------+------------+--------+---------------+------+---------+------+------+-------+
2 rows in set (0.00 sec)
④ SUBQUERY
:SELECT
或 WHERE
列表中包含子查詢
mysql> EXPLAIN SELECT t2.id FROM t2 WHERE t2.id = (SELECT t3.id FROM t3 WHERE t3.id = 1);
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| 1 | PRIMARY | t2 | const | PRIMARY | PRIMARY | 4 | const | 1 | Using index |
| 2 | SUBQUERY | t3 | const | PRIMARY | PRIMARY | 4 | | 1 | Using index |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
2 rows in set (0.00 sec)
⑤ DEPEDENT SUBQUERY
:在 SELECT
或 WHERE
列表中包含子查詢,子查詢基於外層。
mysql> explain select t2.id from t2 where t2.id in (select t3.id from t3 where t3.content = 't3_993');
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
| 1 | PRIMARY | t2 | index | NULL | PRIMARY | 4 | NULL | 1 | Using where; Using index |
| 2 | DEPENDENT SUBQUERY | t3 | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | Using where |
+----+--------------------+-------+-----------------+---------------+---------+---------+------+------+--------------------------+
2 rows in set (0.00 sec)
都在
WHERE
後加入子查詢,SUPERQUERY
是單個值,DEPENDENT SUBQUERY
是一組值。
⑥ UNCACHEABLE SUBQUERY
:無法使用緩存的子查詢。
mysql> explain select * from t3 where id = (select id from t2 where t2.id = @@sort_buffer_size);
+----+----------------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+----------------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE noticed after reading const tables |
| 2 | UNCACHEABLE SUBQUERY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+----------------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
2 rows in set (0.01 sec)
當使用
@@
來引用系統變量時,不會使用緩存。
⑦ UNION
:若第二個 SELECT
出現在 UNION
後,則被標記爲 UNION
;若 UNION
包含在 FROM
子句的子查詢中,外層 SELECT
將被標記爲 DERIVED
mysql> explain select * from t1 union all select * from t2;
+----+--------------+------------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+------+---------------+------+---------+------+------+-------+
| 1 | PRIMARY | t1 | ALL | NULL | NULL | NULL | NULL | 1 | |
| 2 | UNION | t2 | ALL | NULL | NULL | NULL | NULL | 1 | |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | |
+----+--------------+------------+------+---------------+------+---------+------+------+-------+
3 rows in set (0.00 sec)
⑧ UNION RESULT
:從 UNION
表獲取結果的 SELECT
4.2.2.3 table
表示數據所在的表
4.2.2.4 type
查詢的訪問類型;
屬性 | 含義 |
---|---|
system |
表只有一行記錄(等於系統表)是 const 類型的特例,一般不會出現。 |
const |
表示通過索引一次就找到,const 用於比較 primary key 或者 unique 索引。 |
eq_ref |
唯一性索引掃描,對於每個索引鍵,表中只有一條記錄與之匹配。常見於主鍵或者唯一索引掃描。 |
ref |
非唯一性索引掃描,返回匹配某個單獨值的所有行。本質上也是一種索引訪問,它返回所有匹配某個單獨值的行,然而,也可能會找到多個符合條件的行,所以它屬於查找和掃描的混合體。 |
range |
只檢索指定範圍的行,使用一個索引來選擇行。key 列顯示使用了那個索引一般就在 WHERE 語句中出現了 between、<、>、in 等的查詢這種範圍掃描比全表掃描要好,因爲它只需要開始於索引的某一點,結束於另一點,不需要掃描全部索引。 |
index |
出現 index 是 SQL 語句使用了索引但是沒使用索引進行過濾,一般是使用了覆蓋索引或者利用索引進行了排序分組。 |
ALL |
全表掃描以找到匹配的行 |
index_merge |
在查詢中需要用到多個索引組合使用,通常出現在有 OR 關鍵字的 SQL 中。 |
ref_or_null |
對某個字段即需要關聯條件,也需要 null 值的情況下,查詢優化器會選擇用 ref_or_null 連接查詢。 |
index_subquery |
利用索引關聯子查詢,不再全表掃描。 |
unique_subquery |
類似於 index_subquery 子查詢中的唯一索引。 |
查詢的訪問類型,是比較重要的一個指標,結果值從最好好到最壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merege > unique_subquery > index_subquery > range > index > ALL
一般而言要保證查詢至少達到
range
級別,最好能達到ref
index
和ALL
的區別:雖然
index
和ALL
都是讀全表,但是index
是從索引中讀取的,只遍歷索引樹,而ALL
是從硬盤中讀取的,相比較而言索引文件通常比數據文件小,因此index
比ALL
更快。
4.2.2.5 possible_keys
顯示可能應用在這張表中的索引,一個或多個。查詢涉及到的字段上若存在索引,則該索引被列出,但不一定被查詢實際使用。
4.2.2.6 key
實際使用到的索引,如果爲 NULL
則表示沒有使用索引;
查詢中若使用了覆蓋索引,則該索引僅出現在 key
列表中;
4.2.2.7 key_len
表示索引中使用的字節數,可以通過該列計算查詢中使用的索引長度。key_len
字段能幫助檢查是否充分利用索引。
key_len
越長說明索引使用越充分,即使用的索引數越多,匹配越精確;在不損失精度的情況下,長度越短越好;
key_len
的計算方式:
① 首先查看索引上字段的類型和長度,比如:int = 4; varchar(20) = 20; char(20) = 20
;
② varchar、char
這種字符串字段,不同的字符集要乘以不同的值,如:utf8
要乘以 3,GBK
要乘以 2;
③ varchar
這樣的動態字符要加 2 個字節;
④ 允許爲空的字段要加 1 個字節;
列類型 | key_len |
備註 |
---|---|---|
int |
4 + 1 | 允許 NUll ,加 1 個字節 |
int not null |
4 | 不允許爲 NULL |
char(30) utf8 |
30 * 3 + 1 | 允許爲 NULL |
varchar(30) not null utf8 |
30 * 3 + 2 | 動態列類型,加 2 個字節 |
varchar(30) utf8 |
30 * 3 + 2 + 1 | 動態列類型,加兩個字節;允許爲 NULL 再加一個字節 |
text(10) utf8 |
30 * 3 + 3 + 1 | text 截取部分,被視作動態列類型,且允許爲 NULL |
4.2.2.8 ref
顯示索引的那一列被使用,如果可能的話是個 const
(常量)。那些列或常數被用於查找索引列上的值。
4.2.2.9 rows
根據表統計信息和索引選用情況,大致估算出找到所需記錄所需讀取的行數,行數越少越好。
4.2.2.10 Extra
包含不適合在其他列中顯示但是十分重要的額外信息。
① Using filesort
MySQL 會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。MySQL 無法利用索引完成的排序操作稱爲**“文件排序”**
查詢中排序的字段,排序字段若是通過索引去訪問將大大提高排序速度。
② Using temporary
使用了臨時表保存中間結果,MySQL 在對查詢結果排序時使用臨時表。常見於排序 ORDER BY
和分組查詢 GROUP BY
排序
ORDER BY
和分組查詢GROUP BY
是拖慢 SQL 執行的元兇,臨時表會增加 SQL 負擔。
③ Using index
表示相應的 SELECT
操作中使用了覆蓋索引(Covering Index)避免訪問了表的數據行,效率不錯。如果同時出現 Using where
,表明索引被用來執行索引鍵值的查找;如果沒有同時出現 Using where
,表明索引只是用來讀取數據而非利用索引執行查找。利用索引進行了排序或分組。
覆蓋索引(Covering Index):
SELECT
的數據列只用從索引中就能獲取,不需要讀取數據行,MySQL 可以利用索引返回SELECT
列表中的字段,而不需要根據索引再次讀取數據文件,也就是說查詢列要被所建立的索引覆蓋。- 索引是高效找到行的一個方法,但是一般數據庫也能夠使用索引找到一個列的數據,因此它不必讀取整個行。畢竟索引葉子節點存儲了他們索引的數據;當能通過索引就可以得到需要的數據,就不需要讀取行了。索引包含了(或者覆蓋了)滿足查詢結果的數據就叫覆蓋索引;
- 簡單的講覆蓋索引就是
SELECT
中查詢的字段和順序與建立的複合索引順序和字段剛好是一樣的;注意:
如果要使用覆蓋索引,一定要注意
SELECT
列表中取出需要的列,不能使用SELECT *
,因爲如果將所有字段一起做索引會導致索引文件過大,查詢性能下降。
Using filesort
:採用文件排序,影響 SQL 執行效率;
Using temporary
:使用了臨時表,嚴重影響 SQL 執行效率;
Using index
:使用了覆蓋索引,效率不錯;以上三個參數是判斷 SQL 執行效率的基本方法。
④ Using where
表明使用了 WHERE
過濾。
⑤ Using join buffer
表明使用了連接緩存。
⑥ impossible where
WHERE
子句的值總是 false
,不能用來獲取任何元組。
⑦ select tables optimized away
在沒有 GROUP BY
子句的情況下,基於索引優化 MIN/MAX 操作或者對於 MyISAM
存儲引擎優化 COUNT(*)
操作,不必等到執行階段再進行計算,查詢執行計劃生成階段即完成優化。
⑧ distinct
優化 distinct
,再找到第一匹配元組後即停止查找同樣值的工作。
5. 索引優化
5.1 索引分析
5.1.1 單表優化分析
① 創建數據表
CREATE TABLE IF NOT EXISTS `article`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT (10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL ,
`views` INT(10) UNSIGNED NOT NULL ,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL
);
INSERT INTO `article`(`author_id`,`category_id` ,`views` ,`comments` ,`title` ,`content` )VALUES
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(3,3,3,3,'3','3');
創建後的數據庫
mysql> select * from article;
+----+-----------+-------------+-------+----------+-------+---------+
| id | author_id | category_id | views | comments | title | content |
+----+-----------+-------------+-------+----------+-------+---------+
| 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 | 3 | 3 | 3 |
+----+-----------+-------------+-------+----------+-------+---------+
3 rows in set (0.00 sec)
② 查詢需求
查詢 category_id
爲 1 且 comments
大於等於 1 的情況下,views
最多的 article_id
mysql> select id,author_id from article where category_id = 1 AND comments >= 1 order by views desc limit 1;
+----+-----------+
| id | author_id |
+----+-----------+
| 1 | 1 |
+----+-----------+
1 row in set (0.00 sec)
③ 分析優化 SQL
SQL 查詢中存在的問題:
- SQL 進行了全表掃描;
- 沒有也沒有使用索引;
- 使用了文件排序效率低下;
④ 解決方案
創建索引
create index idx_article_ccv on article(category_id, comments, views);
alter table `article` add index idx_article_ccv(`category_id`,`comments`,`views`)
查看索引
mysql> show index from article;
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| article | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 1 | category_id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 2 | comments | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 3 | views | A | 3 | NULL | NULL | | BTREE | | |
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 rows in set (0.00 sec)
查看性能
優化後可以看到查詢已經使用了索引,但是依舊存在文件排序問題。
建立了索引但是沒有完全使用的原因:
根據B-Tree 索引的工作原理
① 先排序
category_id
;② 如果遇到相同的
category_id
則再排序comments
;③ 如果遇到相同的
comments
則再排序views
;④ 當
comments
處於聯合索引中間位置時,因comments >= 1
條件是範圍值(即爲range
)⑤ MySQL 無法再利用索引後面的
views
部分進行檢索;⑥ 最終 MySQL 使用的索引類型是
range
,後面的索引失效;
優化索引
# 刪除不合適的索引
mysql> drop index idx_article_ccv on article;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 創建新的索引
mysql> create index idx_article_cv on article(category_id, views);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 查看新索引
mysql> show index from article;
+---------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| article | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_cv | 1 | category_id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_cv | 2 | views | A | 3 | NULL | NULL | | BTREE | | |
+---------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 rows in set (0.00 sec)
查看性能
使用新索引後
type
類型變成了ref
,使用了索引idx_article_cv
且兩個索列都被使用,而且不會出現文件排序問題,SQL 性能很好。
5.1.2 兩表優化分析
兩表關聯索引加在哪裏?
① 創建數據表
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
CREATE TABLE IF NOT EXISTS `book`(
`bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
創建後的數據表
mysql> select * from book;
+--------+------+
| bookid | card |
+--------+------+
| 1 | 1 |
| 2 | 16 |
| 3 | 18 |
| 4 | 2 |
| 5 | 13 |
| 6 | 19 |
| 7 | 18 |
| 8 | 10 |
| 9 | 17 |
| 10 | 12 |
| 11 | 10 |
| 12 | 13 |
| 13 | 15 |
| 14 | 14 |
| 15 | 6 |
| 16 | 6 |
| 17 | 11 |
| 18 | 17 |
| 19 | 10 |
| 20 | 20 |
+--------+------+
20 rows in set (0.00 sec)
mysql> select * from class;
+----+------+
| id | card |
+----+------+
| 1 | 10 |
| 2 | 3 |
| 3 | 4 |
| 4 | 9 |
| 5 | 14 |
| 6 | 4 |
| 7 | 16 |
| 8 | 10 |
| 9 | 19 |
| 10 | 6 |
| 11 | 11 |
| 12 | 17 |
| 13 | 12 |
| 14 | 6 |
| 15 | 16 |
| 16 | 1 |
| 17 | 15 |
| 18 | 10 |
| 19 | 8 |
| 20 | 9 |
+----+------+
20 rows in set (0.00 sec)
② 實際問題
兩表關聯時索引的建立位置:
- 左連接時在左表還是右表建立索引?
- 右連接時在左表還是右表建立索引?
② 分析優化
左連接時 class 作爲左表,book 作爲右表
分析可知查詢進行全表掃描,且沒有使用索引。
③ 解決方案
給 class 表建立索引
# 在class表建立索引
mysql> create index idx_class_card on class(card);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 查詢索引
mysql> show index from class;
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| class | 0 | PRIMARY | 1 | id | A | 20 | NULL | NULL | | BTREE | | |
| class | 1 | idx_class_card | 1 | card | A | 20 | NULL | NULL | | BTREE | | |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
給左連接的左表加索引後,
type
達到index
級別,但是掃描的行數沒有減少。
爲 book 表建立索引
mysql> drop index idx_class_card on class;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> create index idx_book_card on book(card);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show index from book;
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| book | 0 | PRIMARY | 1 | bookid | A | 20 | NULL | NULL | | BTREE | | |
| book | 1 | idx_book_card | 1 | card | A | 20 | NULL | NULL | | BTREE | | |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
給左連接的右表添加索引,
type
達到ref
級別,且使用了索引,右表掃描的行數減少。出現以上情況的原因是由左連接的特性導致的:
LEFT JOIN
條件用於確定如何讓右表的搜索行,左表是一定全都包含的。- 因此右表是我們的搜索關鍵表,需要建立索引。
同理可推出右連接存在同樣的問題,因此得出結論:左連接時在右表建立索引,右連接在左表建立索引,即**“連接索引相反建立”**
5.1.3 三表優化分析
① 創建數據表
CREATE TABLE IF NOT EXISTS `phone`(
`phoneid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
)ENGINE = INNODB;
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
mysql> select * from phone;
+---------+------+
| phoneid | card |
+---------+------+
| 1 | 18 |
| 2 | 13 |
| 3 | 10 |
| 4 | 12 |
| 5 | 8 |
| 6 | 5 |
| 7 | 2 |
| 8 | 11 |
| 9 | 12 |
| 10 | 4 |
| 11 | 3 |
| 12 | 5 |
| 13 | 13 |
| 14 | 11 |
| 15 | 13 |
| 16 | 15 |
| 17 | 15 |
| 18 | 7 |
| 19 | 10 |
| 20 | 7 |
+---------+------+
20 rows in set (0.00 sec)
② 查詢需求
三表關聯索引建立的位置?
③ 分析優化
三表關聯沒有使用索引且進行了全表掃描。
④ 解決方案
通過兩表關聯可知需要和連接建立相反方向的索引,因此需要在 book
表和 phone
表的 card
字段上建立索引。
# 在 book 表建立索引
mysql> create index idx_book_card on book(card);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 在 phone 表建立索引
mysql> alter table phone add index idx_phone_card(card);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 查詢索引
mysql> show index from book;
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| book | 0 | PRIMARY | 1 | bookid | A | 20 | NULL | NULL | | BTREE | | |
| book | 1 | idx_book_card | 1 | card | A | 20 | NULL | NULL | | BTREE | | |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
mysql> show index from pnone;
ERROR 1146 (42S02): Table 'base_crud.pnone' doesn't exist
mysql> show index from phone;
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| phone | 0 | PRIMARY | 1 | phoneid | A | 20 | NULL | NULL | | BTREE | | |
| phone | 1 | idx_phone_card | 1 | card | A | 20 | NULL | NULL | | BTREE | | |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
查詢分析
加入索引後可以看到查詢使用了索引,且表掃描的行數下降。
5.1.4 關聯查詢優化總結
① 儘可能減少 Join
語句中的 NestedLoop
的循環次數;“用小結果集驅動大結果集”
② 優先優化 NestedLoop
的內層字段;
③ 當無法保證驅動表的 JOIN
字段被索引且內存充足的條件下,可以適當調整 JoinBuffer
設置;
5.2 索引失效
MySQL 中導致索引失效的情況:
① 在索引上做操作(計算、函數、手動/自動類型轉化)會導致索引失效轉而進行全表掃描;
② SQL 中使用 (!= 或 <>)
會導致索引失效轉而進行全表掃描;
③ SQL 中使用 IS NOT NULL
會導致索引失效轉而進行全表掃描;
④ SQL 中 LIKE
以通配符開頭 LIKE(&字符串)
會導致索引失效轉而進行全表掃描;
⑤ SQL 中字符串不加單引號會導致索引失效轉而進行全表掃描;
⑥ SQL 中使用 OR
進行連接會導致索引失效轉而進行全表掃描;
5.2.1 儘量使用全值匹配
SQL 語句中 WHERE
查詢的字段按照順序在索引中可以全部匹配。
SQL 中查詢字段的順序,跟使用索引中字段的順序沒有關係。優化器會在不影響 SQL 執行結果的前提下,進行自動優化。
5.2.2 最佳左前綴法則
SQL 中的查詢字段和索引建立順序不同會導致索引無法使用,甚至索引失效。因此 SQL 中進行條件過濾時需要按照建立索引時的順序,依次匹配跳過某個字段索引後面的字段都無法使用。如果使用複合索引,要遵守最左前綴法則。
最左前綴法則:查詢從索引最左列開始並且不跳過索引中的列。
① 查詢時完全按照索引建立順序,可以看到索引被使用且查詢均爲常量
② 不按照索引順序進行查詢且不完全使用索引列,可以看到索引沒有完全使用
③ 不使用索引列首位進行查詢,可以看到完全沒有使用索引進行了全表掃描
5.2.3 索引列上不做任何操作
計算、函數、(自動/手動)類型轉換,會導致索引失效轉向全表掃描。
SQL 中 WHERE
查詢字符串不加單引號 SQL 會進行字符串轉化,導致索引失效。
5.2.4 索引列上不進行範圍查詢
存儲引擎不能使用索引範圍條件右邊的列,所以將可能做範圍查詢的字段索引順序放到最後。
5.2.5 儘量使用覆蓋索引
只訪問索引的查詢(索引列和查詢列保持一致)減少 SELECT *
操作
5.2.6 索引列上不做判空操作
MySQL 在使用 (!= 或 <>)
時會導致索引失效,導致進行全表掃描
雖然判空操作會導致索引失效,但是具體情況需要具體分析,SQL 查詢不能僅考慮索引是否失效。
5.2.7 索引列上不做非空查詢
SQL 中在表字段允許爲 NULL
的情況下,WHERE
條件中使用 IS NULL
不會導致索引失效,但是 IS NOT NULL
會導致索引失效。
不要進行字段非空判斷,最好給字段設置默認值。
5.2.8 正確使用模糊查詢
SQL 中 LIKE
以通配符開頭 ('%abc...')
MySQL 會導致索引失效,轉而進行全表掃描。即使用 LIKE
的進行模糊匹配時,左模糊和全模糊會導致索引失效,右模糊才能使用索引。
面試題目:解決
LIKE '%字符%'
索引不被使用的方式?建立覆蓋索引解決全模糊導致索引失效問題。如下圖在
name、age
字段上建立索引,再進行全模糊查詢時可以使用到覆蓋索引。
5.2.9 注意連接的使用
使用 OR
連接會導致索引失效,儘量使用 UNION 或 UNION ALL
替代。
5.2.10 索引優化總結
全值匹配我最愛,最左原則要遵守。
帶頭大哥不能死,中間兄弟不能斷。
索引列上少計算,範圍之後全失效。
LIKE
百分寫最右,覆蓋索引不寫 *
。
不等空值還有 OR
,索引影響要注意。
VAR
引號不可丟,SQL 優化有訣竅。
5.2.11 索引優化面試題
① 創建數據庫
create table test(
id int primary key not null auto_increment,
c1 char(10),
c2 char(10),
c3 char(10),
c4 char(10),
c5 char(10));
insert into test(c1,c2,c3,c4,c5) values ('a1','a2','a3','a4','a5');
insert into test(c1,c2,c3,c4,c5) values ('b1','b2','b3','b4','b5');
insert into test(c1,c2,c3,c4,c5) values ('c1','c2','c3','c4','c5');
insert into test(c1,c2,c3,c4,c5) values ('d1','d2','d3','d4','d5');
insert into test(c1,c2,c3,c4,c5) values ('e1','e2','e3','e4','e5');
create index idx_test03_c1234 on test03(c1,c2,c3,c4);
mysql> select * from test;
+----+------+------+------+------+------+
| id | c1 | c2 | c3 | c4 | c5 |
+----+------+------+------+------+------+
| 1 | a1 | a2 | a3 | a4 | a5 |
| 2 | b1 | b2 | b3 | b4 | b5 |
| 3 | c1 | c2 | c3 | c4 | c5 |
| 4 | d1 | d2 | d3 | d4 | d5 |
| 5 | e1 | e2 | e3 | e4 | e5 |
+----+------+------+------+------+------+
5 rows in set (0.00 sec)
② 創建索引
# 創建索引
mysql> create index idx_test_c1234 on test(c1,c2,c3,c4);
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 查詢索引
mysql> show index from test;
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| test | 0 | PRIMARY | 1 | id | A | 5 | NULL | NULL | | BTREE | | |
| test | 1 | idx_test_c1234 | 1 | c1 | A | 5 | NULL | NULL | YES | BTREE | | |
| test | 1 | idx_test_c1234 | 2 | c2 | A | 5 | NULL | NULL | YES | BTREE | | |
| test | 1 | idx_test_c1234 | 3 | c3 | A | 5 | NULL | NULL | YES | BTREE | | |
| test | 1 | idx_test_c1234 | 4 | c4 | A | 5 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
5 rows in set (0.00 se
③ 分析以下 SQL 執行情況
1)基本查詢
explain select * from test where c1 = 'a1';
explain select * from test where c1 = 'a1' and c2 = 'a2';
explain select * from test where c1 = 'a1' and c2 = 'a2' and c3 = 'a3';
explain select * from test where c1 = 'a1' and c2 = 'a2' and c3 = 'a3' and c4 = 'a4';
以上 SQL 均可以使用索引,且使用的索引列逐漸增加。
2)基本查詢
explain select * from test where c1 = 'a1' and c2 = 'a2' and c3 = 'a3' and c4 = 'a4';
explain select * from test where c1 = 'a1' and c3 = 'a3' and c2 = 'a2' and c4 = 'a4';
explain select * from test where c4 = 'a4' and c3 = 'a3' and c2 = 'a2' and c1 = 'a1';
以上的 SQL 查詢均使用四個索引列,原因在於 MySQL 內部優化器進行 SQL 優化,但是建議索引順序和查詢順序保持一致。
3)範圍查詢
explain select * from test where c1 = 'a1' and c2 = 'a2' and c3 > 'a3' and c4 = 'a4';
以上的 SQL 只能應用索引列 c1、c2、c3因爲 c3 處進行範圍查詢導致索引失效。
explain select * from test where c1 = 'a1' and c2 = 'a2' and c4 > 'a4' and c3 = 'a3';
以上的 SQL 應用索引列 c1、c2、c3,c4 因爲 c4 處進行範圍查詢導致索引失效,但是 c4 是最後一個索引列,雖然是範圍查找,但是仍使用了全部的索引列。
4)單值排序查詢
explain select * from test where c1 = 'a1' and c2 = 'a2' and c4 = 'a4' order by c3;
以上 SQL 實際使用了三個索引列 c1、c2、c3,但是僅顯示使用了 c1、c2 實際情況是 c1、c2 參與查詢操作, a3 參與排序操作。由此引出索引的兩大作用,查詢和排序。
explain select * from test where c1 = 'a1' and c2 = 'a2' order by c3;
與上面的 SQL 執行情況一致。
explain select * from test where c1 = 'a1' and c2 = 'a2' order by c4;
上面的 SQL 會出現
filesort
,原因在於複合索引在 a3 處斷裂,導致後續的 a4 只能進行文件排序
5)多值排序查詢
explain select * from test where c1 = 'a1' and c5 = 'a5' order by c2,c3;
以上 SQL 僅使用到 c1 索引列,實際上 c2、c3 被用作於排序。
explain select * from test where c1 = 'a1' and c5 = 'a5' order by c3,c2;
以上的 SQL 會出現
filesort
,原因在於索引在 c2 處斷裂導致索引失效轉而進行文件排序。
explain select * from test where c1 = 'a1' and c2 = 'a2' order by c2,c3;
應用兩個索引列 c1、c2,同時 c2、c3 參與排序
explain select * from test where c1 = 'a1' and c2 = 'a2' and c5 = 'a5' order by c2,c3;
與上一條 SQL 執行情況一致。
explain select * from test where c1 = 'a1' and c2 = 'a2' and c5 = 'a5' order by c3,c2;
結合 MySQL 的特性可知,c2 沒有參與排序只有 c3 參與了排序,實際使用索引列 c1、c2 進行查找,c3 進行排序索引沒有失效。
原因:MySQL 中當排序的值是個常量時,這個常量不會參與排序。
explain select * from test where c1 = 'a1' and c5 = 'a5' order by c3,c2;
以上 SQL 會出現
filesort
,僅使用索引列 c1 進行查詢,在排序使用 c3 時因爲索引在 c2 處斷裂,導致索引失效轉而進行文件排序。
6)分組查詢
explain select * from test where c1 = 'a1' and c4 = 'a4' group by c2,c3;
查詢時使用索引列 c1 分組時使用索引列 c2、c3 索引沒有失效。
explain select * from test where c1 = 'a1' and c4 = 'a4' group by c3,c2;
以上 SQL 會出現
filesort
和temporary
,僅使用索引列 c1 進行查詢,在分組時使用 c3 時因爲索引在 c2 處斷裂,導致索引失效轉而進行文件排序。同時由於對c3 進行分組操作,MySQL 在分組前會先進行排序導致出現臨時表。
④ 面試題總結
- 定值、範圍實際上還是排序,通常
order by
給定的是個範圍; group by
一般都需要進行排序,會產生臨時表;
5.3 優化建議
① 對於單鍵索引,儘量選擇針對當前 query
過濾性更好的字段;
② 在選擇組合索引時,當前 query
中過濾性最好的字段在索引字段順序中越靠前越好;
③ 在選擇組合索引時,儘量選擇能包含當前 query
中 WHERE
子句中更多字段的索引;
④ 儘可能通過分析統計信息和調整 query
寫法來達到選擇合適索引的目的;