一次很是有趣的 SQL 優化經歷

閱讀本文大概須要 6 分鐘。mysql

前言程序員

在網上刷到一篇數據庫優化的文章,本身也來研究一波。sql

場景數據庫

數據庫版本:5.7.25 ,運行在虛擬機中。編程

課程表緩存

#課程表
create table Course(
c_id int PRIMARY KEY,
name varchar(10)
)

增長 100 條數據性能優化

#增長課程表100條數據
DROP PROCEDURE IF EXISTS insert_Course;
DELIMITER $
CREATE PROCEDURE insert_Course()
BEGIN
    DECLARE i INT DEFAULT 1;
        WHILE i<=100 DO
        INSERT INTO Course(\`c_id\`,\`name\`) VALUES(i, CONCAT('語文',i+''));
        SET i = i+1;
    END WHILE;
END $
CALL insert_Course();

運行耗時微信

CALL insert_Course();
> OK
> 時間: 0.152s

課程數據架構

學生表函數

#學生表
create table Student(
s_id int PRIMARY KEY,
name varchar(10)
)

增長 7W 條數據

#學生表增長70000條數據
DROP PROCEDURE IF EXISTS insert_Student;
DELIMITER $
CREATE PROCEDURE insert_Student()
BEGIN
    DECLARE i INT DEFAULT 1;
        WHILE i<=70000 DO
        INSERT INTO Student(\`s_id\`,\`name\`) VALUES(i, CONCAT('張三',i+''));
        SET i = i+1;
    END WHILE;
END $
CALL insert_Student();

運行結果

CALL insert_Student();
> OK
> 時間: 175.838s

學生數據

成績表

#成績表
CREATE table Result(
r_id int PRIMARY KEY,
s_id int,
c_id int,
score int
)

增長 70W 條數據

#成績表增長70W條數據
DROP PROCEDURE IF EXISTS insert_Result;
DELIMITER $
CREATE PROCEDURE insert_Result()
BEGIN
    DECLARE i INT DEFAULT 1;
        DECLARE sNum INT DEFAULT 1;
        DECLARE cNum INT DEFAULT 1;
        WHILE i<=700000 DO
                if (sNum%70000 = 0) THEN
                    set sNum = 1;
                elseif (cNum%100 = 0) THEN 
                    set cNum = 1;
                end if;
        INSERT INTO Result(\`r\_id\`,\`s\_id\`,\`c_id\`,\`score\`) VALUES(i,sNum ,cNum , (RAND()*99)+1);
        SET i = i+1;
                SET sNum = sNum+1;
                SET cNum = cNum+1;
    END WHILE;
END $
CALL insert_Result();

運行結果

CALL insert_Result();
> OK
> 時間: 2029.5s

成績數據

測試

業務需求

查找 語文1 成績爲 100 分的考生

查詢語句

#查詢語文1考100分的考生
select s.* from Student s where s.s_id in 
(select s\_id from Result r where r.c\_id = 1 and r.score = 100)

執行時間:0.937s

查詢結果:32 位知足條件的學生

用了 0.9s ,來查看下查詢計劃:

EXPLAIN
select s.* from Student s where s.s_id in 
(select s\_id from Result r where r.c\_id = 1 and r.score = 100)

發現沒有用到索引,type 全是 ALL ,那麼首先想到的就是創建一個索引,創建索引的字段固然是在 where 條件的字段了。

查詢結果中 type 列:all 是全表掃描,index 是經過索引掃描。

先給 Result 表的 c_id 和 score 創建個索引

CREATE index result\_c\_id\_index on Result(c\_id);

CREATE index result\_score\_index on Result(score);

再次執行上述查詢語句,時間爲:0.027s

快了 34.7 倍(四捨五入),大大縮短了查詢的時間,看來索引能極大程度的提升查詢效率,在合適的列上面創建索引頗有必要,不少時候都忘記創建索引,數據量小的時候沒什麼感受,這優化的感受很 nice 。

相同的 SQL 語句屢次執行,你會發現第一次是最久的,後面執行所需的時間會比第一次執行短些許,緣由是,相同語句第二次查詢會直接從緩存中讀取。

0.027s 很短了,可是還能再進行優化嗎,仔細看下執行計劃:

查看優化後的 SQL :

SELECT
    \`example\`.\`s\`.\`s\_id\` AS \`s\_id\`,
    \`example\`.\`s\`.\`name\` AS \`name\` 
FROM
    \`example\`.\`Student\` \`s\` semi
    JOIN ( \`example\`.\`Result\` \`r\` ) 
WHERE
    (
    ( \`example\`.\`s\`.\`s\_id\` = `<subquery2>`.\`s\_id\` ) 
    AND ( \`example\`.\`r\`.\`score\` = 100 ) 
    AND ( \`example\`.\`r\`.\`c_id\` = 1 ) 
    )

怎麼查看優化後的語句呢?

方法以下(在命令窗口執行):

#先執行
EXPLAIN
select s.* from Student s where s.s_id in 
(select s\_id from Result r where r.c\_id = 1 and r.score = 100);
#在執行
show warnings;

結果以下

有 type = all

按照以前的想法,該 SQL 執行的順序是執行子查詢

select s\_id from Result r where r.c\_id = 1 and r.score = 100

耗時:1.402s

獲得以下結果(部分)

而後在執行

select s.* from Student s where s.s_id in 
(12871,40987,46729,61381,3955,10687,14047,26917,28897,31174,38896,56518,10774,25030,9778,12544,24721,27295,60361,
38479,46990,66988,6790,35995,46192,47578,58171,63220,6685,67372,46279,64693)

耗時:0.222s

比一塊兒執行快多了,查看優化後的 SQL 語句,發現MySQL 居然不是先執行裏層的查詢,而是將 SQL 優化成了 exists 字句,執行計劃中的 select_type 爲 MATERIALIZED(物化子查詢)。MySQL 先執行外層查詢,在執行裏層的查詢,這樣就要循環學生數量知足條件的學生 ID 次,也就是 7W 32 次。

物化子查詢: 優化器使用物化可以更有效的來處理子查詢。物化經過將子查詢結果做爲一個臨時表來加快查詢執行速度,正常來講是在內存中的。mysql 第一次須要子查詢結果是,它物化結果到一張臨時表中。在以後的任何地方須要該結果集,mysql 會再次引用臨時表。優化器也許會使用一個哈希索引來使得查詢更快速代價更小。索引是惟一的,排除重複並使得表數據更少。

那麼改用鏈接查詢呢?

這裏爲了從新分析鏈接查詢的狀況,先暫時刪除索引 result_c_id_index ,result_score_index 。

DROP index result\_c\_id_index on Result;
DROP index result\_score\_index on Result;

鏈接查詢

select s.* from 
Student s 
INNER JOIN Result r 
on r.s\_id = s.s\_id 
where r.c_id = 1 and r.score = 100;

執行耗時:1.293s

查詢結果

用了 1.2s ,來看看執行計劃( EXPLAIN + 查詢 SQL 便可查看該 SQL 的執行計劃):

這裏有連表的狀況出現,我猜測是否是要給 result 表的 s_id 創建個索引

CREATE index result\_s\_id\_index on Result(s\_id);
show index from Result;

在執行鏈接查詢

耗時:1.17s (有點奇怪,按照所看文章的時間應該會變長的)

看下執行計劃:

優化後的查詢語句爲:

SELECT
    \`example\`.\`s\`.\`s\_id\` AS \`s\_id\`,
    \`example\`.\`s\`.\`name\` AS \`name\` 
FROM
    \`example\`.\`Student\` \`s\`
    JOIN \`example\`.\`Result\` \`r\` 
WHERE
    (
    ( \`example\`.\`s\`.\`s\_id\` = \`example\`.\`r\`.\`s\_id\` ) 
    AND ( \`example\`.\`r\`.\`score\` = 100 ) 
    AND ( \`example\`.\`r\`.\`c_id\` = 1 ) 
    )

貌似是先作的鏈接查詢,在進行的 where 條件過濾。

回到前面的執行計劃:

這裏是先作的 where 條件過濾,再作連表,執行計劃還不是固定的,那麼咱們先看下標準的 sql 執行順序:

正常狀況下是先 join 再進行 where 過濾,可是咱們這裏的狀況,若是先 join ,將會有 70W 條數據發送 join ,所以先執行 where 過濾式明智方案,如今爲了排除 mysql 的查詢優化,我本身寫一條優化後的 sql 。

先刪除索引

DROP index result\_s\_id_index on Result;

執行本身寫的優化 sql

SELECT
    s.* 
FROM
    (
        SELECT * FROM Result r WHERE r.c_id = 1 AND r.score = 100 
    ) t
INNER JOIN Student s ON t.s\_id = s.s\_id

耗時爲:0.413s

比以前 sql 的時間都要短。

查看執行計劃

先提取 result 再連表,這樣效率就高多了,如今的問題是提取 result 的時候出現了掃描表,那麼如今能夠明確須要創建相關索引。

CREATE index result\_c\_id\_index on Result(c\_id);
CREATE index result\_score\_index on Result(score);

再次執行查詢

SELECT
    s.* 
FROM
    (
        SELECT * FROM Result r WHERE r.c_id = 1 AND r.score = 100 
    ) t
INNER JOIN Student s ON t.s\_id = s.s\_id

耗時爲:0.044s

這個時間至關靠譜,快了 10 倍。

執行計劃:

咱們會看到,先提取 result ,再連表,都用到了索引。

那麼再來執行下 sql :

EXPLAIN
select s.* from 
Student s 
INNER JOIN Result r 
on r.s\_id = s.s\_id 
where r.c_id = 1 and r.score = 100;

執行耗時:0.050s

執行計劃:

這裏是 mysql 進行了查詢語句優化,先執行了 where 過濾,再執行鏈接操做,且都用到了索引。

擴大測試數據,調整內容爲 result 表的數據增加到 300W ,學生數據更爲分散。

DROP PROCEDURE IF EXISTS insert\_Result\_TO300W;
DELIMITER $
CREATE PROCEDURE insert\_Result\_TO300W()
BEGIN
    DECLARE i INT DEFAULT 700001;
        DECLARE sNum INT DEFAULT 1;
        DECLARE cNum INT DEFAULT 1;
        WHILE i<=3000000 DO
        INSERT INTO Result(\`r\_id\`,\`s\_id\`,\`c_id\`,\`score\`) 
                VALUES(i,(RAND()\*69999)+1 ,(RAND()\*99)+1 , (RAND()*99)+1);
        SET i = i+1;
    END WHILE;
END $
CALL insert\_Result\_TO300W();

更換了一下數據生成的方式,所有采用隨機數格式。

先回顧下:

show index from Result;

執行 sql

select s.* from 
Student s
INNER JOIN Result r
on r.s\_id = s.s\_id
where r.c_id = 81 and r.score = 84;

執行耗時:1.278s

執行計劃:

這裏用到了 intersect 並集操做,即兩個索引同時檢索的結果再求並集,再看字段 score 和 c_id 的區分度,但從一個字段看,區分度都不是很大,從 Result 表檢索,c_id  = 81 檢索的結果是 81 ,score = 84 的結果是 84 。

而 c_id = 81 and score = 84 的結果是 19881,即這兩個字段聯合起來的區分度仍是比較高的,所以創建聯合索引查詢效率將會更高,從另一個角度看,該表的數據是 300W ,之後會更多,就索引存儲而言,都是不小的數目,隨着數據量的增長,索引就不能所有加載到內存,而是要從磁盤讀取,這樣索引的個數越多,讀磁盤的開銷就越大,所以根據具體業務狀況創建多列的聯合索引是必要的,咱們來試試。

DROP index result\_c\_id_index on Result;
DROP index result\_score\_index on Result;
CREATE index result\_c\_id\_score\_index on Result(c_id,score);

指向上述查詢語句

消耗時間:0.025s

這個速度就就很快了,能夠接受。

該語句的優化暫時告一段落。

總結

  • MySQL 嵌套子查詢效率確實比較低
  • 能夠將其優化成鏈接查詢
  • 鏈接表時,能夠先用 where 條件對錶進行過濾,而後作錶鏈接(雖然 MySQL 會對連表語句作優化)
  • 創建合適的索引,必要時創建多列聯合索引
  • 學會分析 sql 執行計劃,mysql 會對 sql 進行優化,全部分析計劃很重要

知識擴展

索引優化

上面講到子查詢的優化,以及如何創建索引,並且在多個字段索引時,分別對字段創建了單個索引。

後面發現其實創建聯合索引效率會更高,尤爲是在數據量較大,單個列區分度不高的狀況下。

單列索引

查詢語句以下:

select * from user\_test\_copy where sex = 2 and type = 2 and age = 10

索引:

CREATE index user\_test\_index\_sex on user\_test_copy(sex);
CREATE index user\_test\_index\_type on user\_test_copy(type);
CREATE index user\_test\_index\_age on user\_test_copy(age);

分別對 sex ,type ,age 字段作了索引,數據量爲300w

查詢時間:0.415s

執行計劃:

發現 type = index_merge

這是mysql對多個單列索引的優化,對結果集採用intersect並集操做

多列索引

多列索引

咱們能夠在這3個列上創建多列索引,將表copy一份以便作測試。

create index user\_test\_index\_sex\_type\_age on user\_test(sex,type,age);

查詢語句:

select * from user_test where sex = 2 and type = 2 and age = 10

執行時間:0.032s

快了10多倍,且多列索引的區分度越高,提升的速度也越多。

執行計劃:

最左前綴

多列索引還有最左前綴的特性:

都會使用到索引,即索引的第一個字段sex要出如今where條件中。

執行一下語句:

select * from user_test where sex = 2
select * from user_test where sex = 2 and type = 2
select * from user_test where sex = 2 and age = 10

索引覆蓋

就是查詢的列都創建了索引,這樣在獲取結果集的時候不用再去磁盤獲取其它列的數據,直接返回索引數據便可

如:

select sex,type,age from user_test where sex = 2 and type = 2 and age = 10

執行時間:0.003s

要比取全部字段快的多

排序

select * from user\_test where sex = 2 and type = 2 ORDER BY user\_name

時間:0.139s

在排序字段上創建索引會提升排序的效率

select * from user\_test where sex = 2 and type = 2 ORDER BY user\_name

最後附上一些sql調優的總結,之後有時間再深刻研究

  • 列類型儘可能定義成數值類型,且長度儘量短,如主鍵和外鍵,類型字段等等
  • 創建單列索引
  • 根據須要創建多列聯合索引
  • 當單個列過濾以後還有不少數據,那麼索引的效率將會比較低,即列的區分度較低,那麼若是在多個列上創建索引,那麼多個列的區分度就大多了,將會有顯著的效率提升。
  • 根據業務場景創建覆蓋索引
  • 只查詢業務須要的字段,若是這些字段被索引覆蓋,將極大的提升查詢效率
  • 多表鏈接的字段上須要創建索引
  • 這樣能夠極大的提升錶鏈接的效率
  • where條件字段上須要創建索引
  • 排序字段上須要創建索引
  • 分組字段上須要創建索引
  • Where條件上不要使用運算函數,以避免索引失效

·END·

程序員的成長之路

路雖遠,行則必至

本文原發於 同名微信公衆號「程序員的成長之路」,回覆「1024」你懂得,給個讚唄。

微信ID:cxydczzl

往期精彩回顧

程序員接私活的7大平臺利器

Java程序員的成長之路

白話TCP爲何須要進行三次握手

Java性能優化的50個細節(珍藏版)

設計電商平臺優惠券系統

一個對話讓你明白架構師是作什麼的?

教你一招用 IDE 編程提高效率的騷操做!

送給程序員們的經典電子書大禮包

相關文章
相關標籤/搜索