SQL使用了索引,為什么還是很慢

數(shù)據(jù)庫的查詢性能一直是性能優(yōu)化的重災(zāi)區(qū)。近期看了丁奇老師的分享“為什么SQL語言使用了索引,但卻還是慢查詢?”,收獲頗豐,以此做個記錄

0 準(zhǔn)備

創(chuàng)建一張測試表,設(shè)置主鍵索引(id)和普通索引(a) ,如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

插入兩行測試數(shù)據(jù)

insert into t values(1,1,1),(2,2,2);

1 定義慢查詢

我們來看一下官網(wǎng)對慢查詢的定義

The slow query log consists of SQL statements that take more than long_query_time seconds to execute and require at least min_examined_row_limit rows to be examined.

mysql的慢查詢與語句的執(zhí)行時間有關(guān),mysql會將語句的執(zhí)行時間與系統(tǒng)設(shè)置的long_query_time這個參數(shù)做對比,如果執(zhí)行時間time > long_query_time并且掃描的行數(shù)超過min_examined_row_limit, 則會被認為是慢查詢,并將該語句到慢查詢?nèi)罩局小?/p>

查看mysql的long_time_time設(shè)置

mysql > show variables like 'long_query_time%';

## 如下所示,慢查詢設(shè)置的時間為10秒
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+

查看慢查詢?nèi)罩镜牡刂?/p>

mysql > show variables  like '%slow_query_log%';

## slow_query_log 表示慢查詢?nèi)罩臼欠耖_啟
## slow_query_log_file 表示慢查詢?nèi)罩镜牡刂?+---------------------+-------------------------------------------------+
| Variable_name       | Value                                           |
+---------------------+-------------------------------------------------+
| slow_query_log      | OFF                                             |
| slow_query_log_file | /usr/local/mysql/data/ali-186590d5fadf-slow.log |
+---------------------+-------------------------------------------------+

2 定義是否使用索引

對于一個語句,是否使用索引的意思是:這個語句在執(zhí)行過程中是否使用到了索引。具體到表象中,是explain一個語句的時候,輸出結(jié)果里key的值不是NULL

mysql > explain select * from t ;

## 如下key為null,表示未使用索引

+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | t     | ALL  | NULL          | NULL | NULL    | NULL |    2 | NULL  |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+

mysql> explain select * from t where id =2;

## 如下key為primary,表示使用了主鍵索引
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | t     | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

mysql> explain select a from t ;
## 如下key為a,表示使用了a這個普通索引
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | t     | index | NULL          | a    | 5       | NULL |    2 | Using index |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+

看上面的結(jié)果可以知道,2和3都使用了索引,但是他們的掃描行數(shù)(rows這一個字段)是不同的,2使用主鍵索引其掃描行數(shù)為1條,3使用了普通索引其掃描的行數(shù)是2條,由于總記錄數(shù)是2條,所以3的方式幾乎是全表掃描了。如果總記錄數(shù)達到100萬條,2的方式還是會很快,而3的方式其掃描的行數(shù)也將達到100萬條,查詢就會很慢。

但是這并不代表2的方式肯定不是慢查詢,因為在極端情況下,如果mysql所在機器的cpu負載很高,2方式查詢的執(zhí)行時間超過了long_query_time,那么它就會被記錄到慢查詢?nèi)罩局小?/p>

3. 慢查詢與索引的關(guān)系

看了上述1,2就明白了,是否是慢查詢與是否使用索引其實并無關(guān)系
慢查詢: 慢查詢與語句的執(zhí)行時間相關(guān),當(dāng)語句的執(zhí)行時間超過long_query_time,那么它就是慢查詢
索引: 是否使用索引描述了語句的執(zhí)行過程

4. 解析使用索引

在InnoDB中,數(shù)據(jù)都是存儲在主鍵索引上的,如圖所示。一行的所有信息都存儲在主鍵索引上(所以InnoDB在查詢過程中必然會掃描主鍵索引樹),而普通索引上只記錄了普通索引與對應(yīng)主鍵索引的映射關(guān)系

image.png

那么在語句執(zhí)行過程中,使用了主鍵索引,就一定不是全表掃描嗎? 不一定,如下,雖然key為primary了,但是掃描行數(shù)還是全表掃描。優(yōu)化器認為該語句需要使用主鍵索引定位到id>0的行,所以認為該執(zhí)行使用了主鍵索引,

mysql> explain select * from t where id >0;
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | t     | range | PRIMARY       | PRIMARY | 4       | NULL |    2 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+

因此,我們需要理清幾個概念:
全表掃描: 我們平時說的使用了全表掃描,指的并不是說語句不使用索引,而是該語句遍歷了整個主鍵索引樹 (select * from t where id > 0)
全索引掃描:指的是語句掃描了整個普通索引樹(select a from t )
索引掃描: 指的是語句使用了索引的快速搜索功能,并且有效地減少了掃描行數(shù)(select * from t where id =2)

4.1 索引的過濾性

除了全索引掃描,還有哪些使用了索引但執(zhí)行速度不夠快的例子呢?

假設(shè)現(xiàn)在你維護了一張表記錄了全國人的基本信息,需要查找所有年齡在10~15歲之間的姓名和基本信息。該語句為:

select * from t_people where age between 10 and 15

一般我們會認為這樣的語句需要在age上建立索引,否則它就會變成全表掃描。但是最后你會發(fā)現(xiàn),即使在age上建立了索引,執(zhí)行的速度還是很慢,因為符合條件了記錄數(shù)有上億行。該語句的執(zhí)行過程如下:

  1. 在age索引上找到age=10的第一個索引
  2. 根據(jù)索引對應(yīng)的主鍵id,回表到主鍵索引上取整行數(shù)據(jù)
  3. 從age索引繼續(xù)向下搜索,重復(fù)步驟2,直到碰到age > 15結(jié)束
image.png

由于上述的執(zhí)行過程中符合條件的記錄數(shù)超過上億行,因此執(zhí)行的時間會很長,它會被記錄到慢查詢中。而上述select a from t雖然也是全索引掃描,但是還是比這個全索引掃描的要快,因此它只掃描了2行。因此,語句執(zhí)行速度的快慢與掃描的行數(shù)息息相關(guān),所以我們在討論索引的時候關(guān)心的是掃描行數(shù),因此索引要有效,就要提高索引的過濾性,減少語句的掃描行數(shù)。上述的age索引過濾性就不夠。

那么過濾性足夠好了,掃描的行數(shù)就一定少呢?

如下有一個(name,age)聯(lián)合索引,如果執(zhí)行語句是 select * from t_people where name ='張三' and age = 8,那么該聯(lián)合索引的過濾性非常不錯,掃描的行數(shù)也會很少,執(zhí)行的效率較高

image.png

但是查詢的過濾性和索引的過濾性不一定是一樣的,如果現(xiàn)在的需求是
select * from t_prople where name like '張%' and age =8
在mysql 5.5及之前的版本中,這個語句的執(zhí)行流程如下:

  1. 在聯(lián)合索引上查找第一個名字為張的聯(lián)合索引
  2. 取出聯(lián)合索引對應(yīng)的主鍵id,回表到主鍵索引中獲取整行速度并對比age是否為8
  3. 繼續(xù)在聯(lián)合索引中向下掃描,重復(fù)2,直到碰到第一個名字不為張結(jié)束

可以看到,上述的執(zhí)行過程中,最耗費時間的就是2步驟的回表。假設(shè)全國第一個名字開頭是張的有8000萬,那么這個執(zhí)行過程就需要回表8000萬次。為什么聯(lián)合索引需要回表取判斷age是否為8? 這是因為在使用聯(lián)合索引的時候,只能使用聯(lián)合索引的最左前綴,這被稱為最左前綴原則。

image.png

在mysql 5.6版本引入了一個優(yōu)化方案,該優(yōu)化方案是在聯(lián)合索引中先判斷age是否為8(將判斷下推到索引搜索過程中),如果是才回表取出整行數(shù)據(jù),假設(shè)名字開頭是張的有8000萬,其中age=8的有1000萬行,那么該查詢只需要回表1000萬次,這會大大減少回表的次數(shù)。

但是該查詢還是要掃描8000萬行的記錄,是否可以繼續(xù)優(yōu)化,減少掃描行數(shù)呢?

我們可以把名字的第一個字和年齡做一個聯(lián)合索引,我們可以使用mysql 5.7引入的新特性:虛擬列,來實現(xiàn)

alter table t_people add name_first varchar(2) generated always as (left(name,1)), add index(name_first,age);

如上,虛擬列name_first永遠自動生成并等于name列的前兩個字節(jié)。并根據(jù)虛擬列生成了一個新的聯(lián)合索引(name_first,age)。有了這個新的聯(lián)合索引,我們就可以通過以下語句來優(yōu)化執(zhí)行語句
select * from t_prople where name_first = '張' and age =8
假設(shè)結(jié)果是1000萬行,那么該語句只需要掃描1000萬行,回表1000萬次。這個優(yōu)化的本質(zhì)是我們創(chuàng)建了一個過濾性更好的索引。

我們使用索引,或者說索引優(yōu)化的過程,實質(zhì)上是減少掃描行數(shù)的過程

5. 問題

5.1 什么是SQL的執(zhí)行時間 ?

sql語句的執(zhí)行時間并不包含其獲取鎖的等待時間,而且慢查詢?nèi)罩静迦氲臅r間是在sql語句釋放所有鎖之后,因此日志順序和執(zhí)行順序很可能是不同的

5.2 我在where中使用了普通索引字段,系統(tǒng)一定會使用全索引掃描嗎?

不一定,系統(tǒng)可能會走全表掃描,這取決與系統(tǒng)的判斷。判斷來源于系統(tǒng)的預(yù)測,也就是說,如果要走 c 字段索引的話,系統(tǒng)會預(yù)測走 c 字段索引大概需要掃描多少行。如果預(yù)測到要掃描的行數(shù)很多,它可能就不走索引而直接掃描全表了。

5.3 如果查詢的字段上有索引,查詢語句一定會使用索引掃描嗎?

不一定,查詢優(yōu)化器在選擇查詢計劃的時候,會對不同查詢計劃進行評估(通過一個cost function),選擇它認為執(zhí)行最快的查詢計劃。

5.4 優(yōu)化器使用的索引不滿意,可以自行指定sql使用的索引嗎?

是可以的,使用use index來指定sql執(zhí)行使用的索引,這是一種優(yōu)化手段,具體可看: https://www.cnblogs.com/edwardlost/archive/2010/12/31/1923105.html

6. 本文摘錄及參考自:

###1. The Slow Query Log - MySQL :: Developer Zone
###2. 為什么我加了索引,SQL執(zhí)行還是這么慢*(一)? - 李佳霖i - 博客園
###3. https://www.zhihu.com/question/49738281?sort=created

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 索引 數(shù)據(jù)庫中的查詢操作非常普遍,索引就是提升查找速度的一種手段 索引的類型 從數(shù)據(jù)結(jié)構(gòu)角度分 1.B+索引:傳統(tǒng)...
    一凡呀閱讀 3,212評論 0 8
  • 面試題5:union all 和 union的區(qū)別 Union:對兩個結(jié)果集進行并集操作,不包括重復(fù)行,同時進行默...
    行者和他的鋼筆閱讀 1,048評論 0 1
  • 一、MySQL優(yōu)化 MySQL優(yōu)化從哪些方面入手: (1)存儲層(數(shù)據(jù)) 構(gòu)建良好的數(shù)據(jù)結(jié)構(gòu)??梢源蟠蟮奶嵘覀僑...
    寵辱不驚丶歲月靜好閱讀 2,654評論 1 8
  • 我終于離開了,舒坦! 我相信你做的決定,是你所能做的最佳選擇。我們?nèi)耍际菚磺榫w左右,被接觸的面所限制的,我也不...
    碎落的面包屑閱讀 294評論 0 1
  • 雪后初晴 窗明幾凈 陽光如詩如歌 沒人知道 有個人一直在等待 如此這般的晌午 愿時光倒流 飛奔至你樓下 懇請麻雀在...
    從此蕭朗是路人閱讀 301評論 0 2

友情鏈接更多精彩內(nèi)容