图片截取及参考文献:
1.https://xiaolincoding.com/mysql/index/index_lose.html#%E7%B4%A2%E5%BC%95%E5%AD%98%E5%82%A8%E7%BB%93%E6%9E%84%E9%95%BF%E4%BB%80%E4%B9%88%E6%A0%B7
2.https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/MySQL%E5%AE%9E%E6%88%98%E5%AE%9D%E5%85%B8/11%20%20%E7%B4%A2%E5%BC%95%E5%87%BA%E9%94%99%EF%BC%9A%E8%AF%B7%E7%90%86%E8%A7%A3%20CBO%20%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.md
3.https://juejin.cn/post/7149074488649318431#heading-1

索引失效及其场景

要了解索引失效,必须了解索引的存储过程和查询原理,因此请务必看完上篇再继续阅读。

接下来,将介绍索引失效的各个场景:

1.对索引使用左或者左右模糊匹配

like %xx 或者 like %xx% 都会导致索引失效,而 like xx%不会。如图所示:

如果是査询 name 前缀为林的用户,那么就会走索引扫描,执行计划中的 type=range 表示走索引扫描key=index_name 看到实际走了 index_name 索引:

原理:B+树索引的有序特性,无法进行二分查找,因此只能进行全表扫描。

2.对索引使用函数

因为索引保存的是索引原始字段的值,不是函数计算后的值,自然没办法使用索引。

不过在mysql8.0版本后,支持对函数建立索引啦!

3.对索引进行表达式计算

在查询条件中对索引进行表达式计算,也是无法走索引的。

比如,下面这条查询语句,执行计划中 type=ALL,说明是通过全表扫描的方式查询数据的:

改成where id = 10 -1就可以:

原理和上面的函数部分类似。

4.对索引隐式类型转换

mysql遇到字符串和数字比较时,会把字符串自动转化为数字进行比较。如果在查询过程中对变量发生了类型上的隐式转换,则会发生索引失效。

这条语句相当于:

1
select * from t user where CAST(phone As signed int)= 1300000001;

因此失效。

反过来,若对变量无影响,则照常走索引:

5.联合索引的非最左匹配

上一篇也有讲到,但是我们这里再着重讲一下索引下推

比如建立了索引(a,b,c),查询where a = 1 and c = 3,还走索引嘛?符合最左匹配嘛?

MySQL 5.5 的话,前面a会走索引,在联合索引找到主键值后,开始回表,到主键索引读取数据行Server 层从存储引擎层获取到数据行后,然后在 Server 层再比对c字段的值。

从 MvSOL5.6之后,有一个索引下推功能,可以在存储引擎层进行索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,再返还给 Server 层,从而减少回表次数。

索引下推的大概原理是:截断的字段不会在 Server 层进行条件判断,而是会被下推到「存储引擎层」进行条件判断(因为c字段的值是在(a,b,c)联合索引里的),然后过滤出符合条件的数据后再返回给Server 层。由于在引擎层就过滤掉大量的数据,无需再回表读取数据来进行判断,减少回表次数,从而提升了性能。

比如下面这条 where a=1 and c=0语句,我们可以从执行计划中的Extra=Using index condition使用了索引下推功能。

6.WHERE句子中的OR

如果or的两个条件其中有一个不是索引,那就会走全表扫描使得索引失效。

其实也很好理解,or的意思是只要满足一个条件就行,那肯定是以最麻烦的那个为准咯。

解决方法?将age设置为索引呗。

可以看到 type=index merge, index merge 的意思就是对id 和 age 分别进行了扫描,然后将这两个结果集进行了合并,这样做的好处就是避免了全表扫描。

索引选择及其标准

很显然,涉及到查询路径的问题那就和server层的sql优化器脱不了关系。这里介绍的是:CBO(Cost-based Optimizer,基于成本的优化器)

在 MySQL中,一条 SQL 的计算成本计算如下所示:

1
2
3
Cost  = Server Cost + Engine Cost

= CPU Cost + IO Cost

其中,CPU Cost 表示计算的开销,比如索引键值的比较、记录值的比较、结果集的排序……这些操作都在 Server 层完成;

IO Cost 表示引擎层 IO 的开销,MySQL 8.0 可以通过区分一张表的数据是否在内存中,分别计算读取内存 IO 开销以及读取磁盘 IO 的开销。

数据库 mysql 下的表 server_cost、engine_cost 则记录了对于各种成本的计算,如:

表 server_cost 记录了 Server 层优化器各种操作的成本,这里面包括了所有 CPU Cost,其具体含义如下。

  • disk_temptable_create_cost:创建磁盘临时表的成本,默认为20。
  • disk_temptable_row_cost:磁盘临时表中每条记录的成本,默认为0.5。
  • key_compare_cost:索引键值比较的成本,默认为0.05,成本最小。
  • memory_temptable_create_cost:创建内存临时表的成本:默认为1。
  • memory_temptable_row_cost:内存临时表中每条记录的成本,默认为0.1。
  • row_evaluate_cost:记录间的比较成本,默认为0.1。

可以看到, MySQL 优化器认为如果一条 SQL 需要创建基于磁盘的临时表,则这时的成本是最大的,其成本是基于内存临时表的 20 倍。而索引键值的比较、记录之间的比较,其实开销是非常低的,但如果要比较的记录数非常多,则成本会变得非常大。

而表 engine_cost 记录了存储引擎层各种操作的成本,这里包含了所有的 IO Cost,具体含义如下。

  • io_block_read_cost:从磁盘读取一个页的成本,默认值为1。
  • memory_block_read_cost:从内存读取一个页的成本,默认值为0.25。

也就是说, MySQL 优化器认为从磁盘读取的开销是内存开销的 4 倍

在知道 MySQL 索引选择是基于 SQL 执行成本之后,接下来,我们就能分析一些索引出错问题到底是怎么回事了。

1.未能使用创建的索引

MySQL 优化器永远是根据成本,选择出最优的执行计划。哪怕是同一条 SQL 语句,只要范围不同,优化器的选择也可能不同。

如下面这两条 SQL:

1
2
3
4
5
6
7
SELECT * FROM orders

WHERE o_orderdate > '1994-01-01' and o_orderdate < '1994-12-31';

SELECT * FROM orders

WHERE o_orderdate > '1994-02-01' and o_orderdate < '1994-12-31';

上面这两条 SQL 都是通过索引字段 o_orderdate 进行查询,然而第一条 SQL 语句的执行计划并未使用索引 idx_orderdate,而是使用了如下的执行计划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
EXPLAIN SELECT * FROM orders 

WHERE o_orderdate > '1994-01-01'

AND o_orderdate < '1994-12-31'\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: orders

partitions: NULL

type: ALL

possible_keys: idx_orderdate

key: NULL

key_len: NULL

ref: NULL

rows: 5799601

filtered: 32.35

Extra: Using where

从上述执行计划中可以发现,优化器已经通过 possible_keys 识别出可以使用索引 idx_orderdate,但最终却使用全表扫描的方式取出结果。 最为根本的原因在于:优化器认为使用通过主键进行全表扫描的成本比通过二级索引 idx_orderdate 的成本要低,可以通过 FORMAT=tree 观察得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
EXPLAIN FORMAT=tree 

SELECT * FROM orders

WHERE o_orderdate > '1994-01-01'

AND o_orderdate < '1994-12-31'\G

*************************** 1. row ***************************

EXPLAIN: -> Filter: ((orders.O_ORDERDATE > DATE'1994-01-01') and (orders.O_ORDERDATE < DATE'1994-12-31')) (cost=592267.11 rows=1876082)

-> Table scan on orders (cost=592267.11 rows=5799601)

EXPLAIN FORMAT=tree

SELECT * FROM orders FORCE INDEX(idx_orderdate)

WHERE o_orderdate > '1994-01-01'

AND o_orderdate < '1994-12-31'\G

*************************** 1. row ***************************

EXPLAIN: -> Index range scan on orders using idx_orderdate, with index condition: ((orders.O_ORDERDATE > DATE'1994-01-01') and (orders.O_ORDERDATE < DATE'1994-12-31')) (cost=844351.87 rows=1876082)

可以看到,MySQL 认为全表扫描,然后再通过 WHERE 条件过滤的成本为 592267.11,对比强制使用二级索引 idx_orderdate 的成本为 844351.87。

成本上看,全表扫描低于使用二级索引。故,MySQL 优化器没有使用二级索引 idx_orderdate。

为什么全表扫描比二级索引查询快呢? 因为二级索引需要回表,当回表的记录数非常大时,成本就会比直接扫描要慢,因此这取决于回表的记录数。

所以,第二条 SQL 语句,只是时间范围发生了变化,但是 MySQL 优化器就会自动使用二级索引 idx_orderdate了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
EXPLAIN SELECT * FROM orders 

WHERE o_orderdate > '1994-02-01'

AND o_orderdate < '1994-12-31'\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: orders

partitions: NULL

type: range

possible_keys: idx_orderdate

key: idx_orderdate

key_len: 3

ref: NULL

rows: 1633884

filtered: 100.00

Extra: Using index condition

2.数据倾斜造成优化器误判

此时可以创立直方图,对mysql优化器进行矫正。

原文如下:

……由于字段 o_orderstatus 仅有三个值,分别为 ‘O’、’P’、’F’。但 MySQL 并不知道这三个列的分布情况,认为这三个值是平均分布的,但其实是这三个值存在严重倾斜:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT o_orderstatus,count(1) 

FROM orders GROUP BY o_orderstatus;

+---------------+----------+

| o_orderstatus | count(1) |

+---------------+----------+

| F | 2923619 |

| O | 2923597 |

| P | 152784 |

+---------------+----------+

因此,优化器会认为订单状态为 P 的订单占用 1⁄3 的数据,使用全表扫描,避免二级索引回表的效率会更高。

然而,由于数据倾斜,订单状态为 P 的数据非常少,根据索引 idx_orderstatus 查询的效率会更高。这种情况下,我们可以利用 MySQL 8.0 的直方图功能,创建一个直方图,让优化器知道数据的分布,从而更好地选择执行计划。直方图的创建命令如下所示:

1
2
3
ANALYZE TABLE orders 

UPDATE HISTOGRAM ON o_orderstatus;

在创建完直方图后,MySQL会收集到字段 o_orderstatus 的数值分布,可以通过下面的命令查询得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SELECT 

v value,

CONCAT(round((c - LAG(c, 1, 0) over()) * 100,1), '%') ratio

FROM information_schema.column_statistics,

JSON_TABLE(histogram->'$.buckets','$[*]' COLUMNS(v VARCHAR(60) PATH '$[0]', c double PATH '$[1]')) hist

WHERE column_name = 'o_orderstatus';

+-------+-------+

| value | ratio |

+-------+-------+

| F | 49% |

| O | 48.5% |

| P | 2.5% |

+-------+-------+

可以看到,现在 MySQL 知道状态为 P 的订单只占 2.5%,因此再去查询状态为 P 的订单时,就会使用到索引 idx_orderstatus了,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
EXPLAIN SELECT * FROM orders 

WHERE o_orderstatus = 'P'\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: orders

partitions: NULL

type: ref

possible_keys: idx_orderstatus

key: idx_orderstatus

key_len: 4

ref: const

rows: 306212

filtered: 100.00

Extra: Using index condition

感觉在实际工作中,应用explain和format=tree等命令进行查询命令的优化是非常重要的必修课!

索引应用

引入索引机制后,能够给数据库带来的优势很明显:

  • ①整个数据库中,数据表的查询速度直线提升,数据量越大时效果越明显
  • ②通过创建唯一索引,可以确保数据表中的数据唯一性,无需额外建立唯一约束。
  • ③在使用分组和排序时,同样可以显著减少SQL查询的分组和排序的时间。
  • 连表查询时,基于主外键字段上建立索引,可以带来十分明显的性能提升。
  • ⑤索引默认是B+Tree有序结构,基于索引字段做范围查询时,效率会明显提高。
  • ⑥从MySQL整体架构而言,减少了查询SQL的执行时间,提高了数据库整体吞吐量。

感觉索引好处很大啊,对于这点确实毋庸置疑,but at what cost?

建立索引的同时也会带来一系列弊端,如:

  • ①建立索引会生成本地磁盘文件,需要额外的空间存储索引数据,磁盘占用率会变高。
  • ②写入数据时,需要额外维护索引结构,增、删、改数据时,都需要额外操作索引。
  • ③写入数据时维护索引需要额外的时间开销,执行写SQL时效率会降低,性能会下降。

其实就是说,数据库很小/频繁插入/删除/更新的数据库都是不太适合索引的。当然,但对数据库整体来说,索引带来的优势会大于劣势。不过也正由于索引存在弊端,它不是越多越好,合理建立索引才是最佳选择。

那么怎么样建立合适的索引呢?其实也没有标准,不过有几条经验之谈:

  • ①查询SQL中尽量不要使用OR关键字,可以使用多SQL或子查询代替。
  • ②模糊查询尽量不要以%开头,如果实在要实现这个功能可以建立全文索引。
  • ③编写SQL时一定要注意字段的数据类型,否则MySQL的隐式转换会导致索引失效。
  • ④一定不要在编写SQL时让索引字段执行计算工作,尽量将计算工作放在客户端中完成。
  • ⑤对于索引字段尽量不要使用计算类函数,一定要使用时请记得将函数计算放在=后面。
  • ⑥多条件的查询SQL一定要使用联合索引中的第一个字段,否则会打破最左匹配原则。
  • ⑦对于需要对比多个字段的查询业务时,可以拆分为连表查询,使用临时表代替。
  • ⑧在SQL中不要使用反范围性的查询条件,大部分反范围性、不等性查询都会让索引失效。
  • .......

实际上无非就是根据前面给出的索引失效情况,尽量让自己编写的SQL不会导致索引失效即可,写出来的SQL能走索引查询,那就能在很大程度上提升数据检索的效率。


索引篇,至此结束!