CategoryResourceRepost/极客时间专栏/MySQL 必知必会/实践篇/11 | 索引:怎么提高查询的速度?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

447 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="11 | 索引:怎么提高查询的速度?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/d8/d0b32336271fe5a52410da5a19ee7fd8.mp3"></audio>
你好,我是朱晓峰。
在我们的超市信息系统刚刚开始运营的时候因为数据量很少每一次的查询都能很快拿到结果。但是系统运转时间长了以后数据量不断地累积变得越来越庞大很多查询的速度就变得特别慢。这个时候我们就采用了MySQL 提供的高效访问数据的方法—— 索引有效地解决了这个问题甚至之前的一个需要8秒钟才能完成的查询现在只用0.3秒就搞定了速度提升了20多倍。
那么,索引到底是啥呢?该怎么使用呢?这节课,我们就来聊一聊。
# 索引是什么?
如果你去过图书馆,应该会知道图书馆的检索系统。图书馆为图书准备了检索目录,包括书名、书号、对应的位置信息,包括在哪个区、哪个书架、哪一层。我们可以通过书名或书号,快速获知书的位置,拿到需要的书。
MySQL中的索引就相当于图书馆的检索目录它是帮助MySQL系统快速检索数据的一种存储结构。我们可以在索引中按照查询条件检索索引字段的值然后快速定位数据记录的位置这样就不需要遍历整个数据表了。而且数据表中的字段越多表中数据记录越多速度提升越是明显。
我来举个例子进一步解释下索引的作用。这里要用到销售流水表demo.trans表结构如下
```
mysql&gt; describe demo.trans;
+---------------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+-------+
| itemnumber | int | YES | MUL | NULL | |
| quantity | text | YES | | NULL | |
| price | text | YES | | NULL | |
| transdate | datetime | YES | MUL | NULL | |
| actualvalue | text | YES | | NULL | |
| barcode | text | YES | | NULL | |
| cashiernumber | int | YES | MUL | NULL | |
| branchnumber | int | YES | MUL | NULL | |
| transuniqueid | text | YES | | NULL | |
+---------------+----------+------+-----+---------+-------+
9 rows in set (0.02 sec)
```
某个门店的销售流水表有400万条数据现在我要查看一下商品编号是100的商品在2020-12-12这一天的销售情况查询代码如下
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (8.08 sec)
```
可以看到结果总共有2条记录可是却花了8秒钟非常慢。同时这里我没有做表的关联这只是单表的查询而且只是一个门店几个月的数据而已。而总部是把所有门店的数据都汇总到一起查询速度更慢这样的查询效率我们肯定是不能接受的。
怎么解决这个问题呢?这时,我们就可以给数据表添加索引。
# 单字段索引
MySQL支持单字段索引和组合索引而单字段索引比较常用我们先来学习下创建单字段索引的方法。
## 如何创建单字段索引?
创建单字段索引一般有3种方式
1. 你可以通过CREATE语句直接给已经存在的表创建索引这种方式比较简单我就不多解释了
1. 可以在创建表的同时创建索引;
1. 可以通过修改表来创建索引。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段);
```
创建表的同时创建索引的语法如下所示:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段)
)
```
修改表时创建索引的语法如下所示:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段);
```
这里有个小问题要提醒你一下给表设定主键约束或者唯一性约束的时候MySQL会自动创建主键索引或唯一性索引。这也是我建议你在创建表的时候一定要定义主键的原因之一。
举个小例子我们可以给表demo.trans创建索引如下
```
mysql&gt; CREATE INDEX index_trans ON demo.trans (transdate(10));
Query OK, 0 rows affected (1 min 8.71 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.30 sec)
```
可以看到加了索引之后这一次我们只用了0.3秒比没有索引的时候快了20多倍。这么大的差距说明索引对提高查询的速度确实很有帮助。那么索引是如何做到这一点的呢下面我们来学习下单字段索引的作用原理。
## 单字段索引的作用原理
要知道索引是怎么起作用的我们需要借助MySQL中的EXPLAIN 这个关键字。
EXPLAIN关键字能够查看SQL语句的执行细节包括表的加载顺序表是如何连接的以及索引使用情况等。
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| 1 | SIMPLE | trans | NULL | range | index_trans | index_trans | 6 | NULL | 5411 | 10.00 | Using index condition; Using where; Using MRR |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
1 row in set, 1 warning (0.00 sec)
```
我来解释下代码里的关键内容。
- type=range表示使用索引查询特定范围的数据记录。
- rows=5411表示需要读取的记录数。
- possible_keys=index_trans表示可以选择的索引是index_trans。
- key=index_trans表示实际选择的索引是index_trans。
- extra=Using index condition;Using where;Using MRR这里面的信息对SQL语句的执行细节做了进一步的解释包含了3层含义第一个是执行时使用了索引第二个是执行时通过WHERE条件进行了筛选第三个是使用了顺序磁盘读取的策略。
通过这个小例子我们可以发现有了索引之后MySQL在执行SQL语句的时候多了一种优化的手段。也就是说在查询的时候可以先通过查询索引快速定位然后再找到对应的数据进行读取这样就大大提高了查询的速度。
## 如何选择索引字段?
在刚刚的查询中我们是选择transdate交易时间字段来当索引字段你可能会问为啥不选别的字段呢这是因为交易时间是查询条件。MySQL可以按照交易时间的限定“2020年12月12日”在索引中而不是数据表中寻找满足条件的索引记录再通过索引记录中的指针来定位数据表中的数据。这样索引就能发挥作用了。
不过你有没有想过itemnumber字段也是查询条件能不能用itemnumber来创建一个索引呢我们来试一试
```
mysql&gt; CREATE INDEX index_trans_itemnumber ON demo.trans (itemnumber);
Query OK, 0 rows affected (43.88 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后看看效果:
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12' -- 对交易时间的筛选可以在transdate的索引中定位
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对商品编号的筛选可以在itemnumber的索引中定位
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.38 sec)
```
我们发现用itemnumber创建索引之后查询速度跟之前差不多基本在同一个数量级。
这是为啥呢我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对itemnumber 进行限定
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans,index_trans_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 0.14 | Using where |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
我们发现“possible_keys= index_trans,index_trans_itemnumber ”就是说MySQL认为可以选择的索引确实有2个一个是用transdate字段创建的索引index_trans另一个是用itemnumber字段创建的索引index_trans_itemnumber。
key= index_trans_itemnumber 说明MySQL实际选择使用的索引是itemnumber字段创建的索引index_trans_itemnumber。而rows=1192就表示实际读取的数据记录数只有1192个比用transdate创建的索引index_trans的实际读取记录数要少这就是MySQL选择使用itemnumber索引的原因。
**所以,我建议你在选择索引字段的时候,要选择那些经常被用做筛选条件的字段**。这样才能发挥索引的作用,提升检索的效率。
# 组合索引
在实际工作中有时会遇到比较复杂的数据表这种表包括的字段比较多经常需要通过不同的字段筛选数据特别是数据表中包含多个层级信息。比如我们的销售流水表就包含了门店信息、收款机信息和商品信息这3个层级信息。门店对应多个门店里的收款机每个收款机对应多个从这台收款机销售出去的商品。我们经常要把这些层次信息作为筛选条件来进行查询。这个时候单字段的索引往往不容易发挥出索引的最大功效可以使用组合索引。
现在先看看单字段索引的效果我们分别用branchnumber和cashiernumber来创建索引
```
mysql&gt; CREATE INDEX index_trans_branchnumber ON demo.trans (branchnumber);
Query OK, 0 rows affected (41.49 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; CREATE INDEX index_trans_cashiernumber ON demo.trans (cashiernumber);
Query OK, 0 rows affected (41.95 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了门店编号和收款机编号的索引,现在我们就尝试一下以门店编号、收款机编号和商品编号为查询条件,来验证一下索引是不是起了作用。
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1 -- 门店编号和收款机号为筛选条件
-&gt; AND itemnumber = 100; -- 商品编号为筛选条件
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.31 sec)
```
结果有6条记录查询时间是0.31秒,跟只创建商品编号索引差不多。下面我们就来查看一下执行计划,看看新建的索引有没有起作用。
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
MySQL有3个索引可以用分别是用branchnumber创建的index_trans_branchnumber、用cashiernumber创建的index_trans_cashiernumber和用itemnumber创建的index_trans_itemnumber。
最后MySQL还是选择了index_trans_itemnumber实际筛选的记录数是1192花费了0.31秒。
为什么MySQL会这样选呢这是因为优化器现在有3种索引可以用分别是商品编号索引、门店编号索引和收款机号索引。优化器发现商品编号索引实际搜索的记录数最少所以最后就选择了这种索引。
所以,**如果有多个索引而这些索引的字段同时作为筛选字段出现在查询中的时候MySQL会选择使用最优的索引来执行查询操作**。
能不能让这几个筛选字段同时发挥作用呢这就用到组合索引了。组合索引就是包含多个字段的索引。MySQL最多支持由16个字段组成的组合索引。
## 如何创建组合索引?
创建组合索引的语法结构与创建单字段索引相同,不同的是相比单字段索引,组合索引使用了多个字段。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段1字段2...);
```
创建表的同时创建索引:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段1字段2...)
)
```
修改表时创建索引:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段1字段2...);
```
现在,针对刚刚的查询场景,我们就可以通过创建组合索引,发挥多个字段的筛选作用。
具体做法是我们给销售流水表创建一个由3个字段branchnumber、cashiernumber、itemnumber组成的组合索引如下所示
```
mysql&gt; CREATE INDEX Index_branchnumber_cashiernumber_itemnumber ON demo.trans (branchnumber,cashiernumber,itemnumber);
Query OK, 0 rows affected (59.26 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了组合索引,刚刚的查询速度就更快了:
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.00 sec)
```
几乎是瞬间就完成了不超过10毫秒。我们看看MySQL的执行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE -- 同时筛选门店编号、收款机号和商品编号
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_branchnumber_cashiernumber_itemnumber | 15 | const,const,const | 6 | 100.00 | NULL |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
```
这个查询MySQL可以用到的索引有4个
- index_trans_itemnumber
- index_trans_branchnumber
- index_trans_cashiernumber
- 我们刚才用branchnumber、cashiernumber和itemnumber创建的组合索引Index_branchnumber_cashiernumber_itemnumber。
MySQL选择了组合索引筛选后读取的记录只有6条。组合索引被充分利用筛选更加精准所以非常快。
## 组合索引的原理
下面我就来讲讲组合索引的工作原理。
**组合索引的多个字段是有序的,遵循左对齐的原则**。比如我们创建的组合索引排序的方式是branchnumber、cashiernumber和itemnumber。因此筛选的条件也要遵循从左向右的原则如果中断那么断点后面的条件就没有办法利用索引了。
比如说我们刚才的条件branchnumber = 11 AND cashiernumber = 1 AND itemnumber = 100包含了从左到右的所有字段所以可以最大限度使用全部组合索引。
假如把条件换成“cashiernumber = 1 AND itemnumber = 100”由于我们的组合索引是按照branchnumber、cashiernumber和itemnumber的顺序建立的最左边的字段branchnumber没有包含到条件当中中断了所以这个条件完全不能使用组合索引。
类似的如果筛选的是一个范围如果没有办法无法精确定位也相当于中断。比如“branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100”这个条件只能用到组合索引中branchnumber&gt;10的部分后面的索引就都用不上了。我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.02 sec)
```
果然MySQL没有选择组合索引而是选择了用itemnumber创建的普通索引index_trans_itemnumber。因为**如果只用组合索引的一部分,效果没有单字段索引那么好**。
# 总结
这节课,我们学习了什么是索引、如何创建和使用索引。索引可以非常显著地提高数据查询的速度,数据表里包含的数据越多,效果越显著。我们应该选择经常被用做筛选条件的字段来创建索引,这样才能通过索引缩小实际读取数据表中数据的范围,发挥出索引的优势。如果有多个筛选的字段,而且经常一起出现,也可以用多个字段来创建组合索引。
如果你要删除索引,就可以用:
```
DROP INDEX 索引名 ON 表名;
```
当然, 有的索引不能用这种方法删除,比如主键索引,你就必须通过修改表来删除索引。语法如下:
```
ALTER TABLE 表名 DROP PRIMARY KEY
```
最后我来跟你说说索引的成本。索引能够提升查询的效率但是建索引也是有成本的主要有2个方面一个存储空间的开销还有一个是数据操作上的开销。
- 存储空间的开销,是指索引需要单独占用存储空间。
- 数据操作上的开销,是指一旦数据表有变动,无论是插入一条新数据,还是删除一条旧的数据,甚至是修改数据,如果涉及索引字段,都需要对索引本身进行修改,以确保索引能够指向正确的记录。
因此,索引也不是越多越好,创建索引有存储开销和操作开销,需要综合考虑。
# 思考题
假如我有一个单品销售统计表,包括门店编号、销售日期(年月日)、商品编号、销售数量、销售金额、成本、毛利,而用户经常需要对销售情况进行查询,你会对这个表建什么样的索引呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。