MySQl 索引优化与查询优化

53 阅读14分钟

第10章_索引优化与查询优化.pdf

数据准备

参考PDF

概述

索引失效案例(脑海生成B+树去理解记忆)

表结构

全值匹配我最爱

最佳左前缀法则

主键插入顺序

可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录

移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量

避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。

所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 , 比如: person_info 表:

CREATE TABLE person_info
(
    id           INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name         VARCHAR(100) NOT NULL,
    birthday     DATE         NOT NULL,
    phone_number CHAR(11)     NOT NULL,
    country      varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);

我们自定义的主键列 id 拥有 AUTO_INCREMENT 属性,在插入记录时存储引擎会自动为我们填入自增的

主键值。这样的主键占用空间小,顺序写入,减少页分裂。


计算、函数、类型转换(自动或手动)导致索引失效

name 字段有索引的情况下,第一个走索引,第二个不走。


类型转换导致索引失效


范围条件右边的列索引失效


不等于(!= 或者<>)索引失效


is null可以使用索引,is not null无法使用索引

like以通配符%开头索引失效


OR 前后存在非索引的列,索引失效



数据库和表的字符集统一使用utf8mb4

统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不

同的 字符集 进行比较前需要进行 转换 会造成索引失效。


练习


小结


关联查询优化

join语句原理

驱动表与被驱动表

join前后并不能决定驱动表或者被驱动表


简单嵌套循环连接(性能最差)

索引嵌套循环连接

块嵌套循环连接(了解)

被驱动表会先加载到内存才能与驱动表进行匹配操作

小结(重点)

子查询优化

MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结

果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。

子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子

查询的执行效率不高。

原因:

① 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表

中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会

受到一定的影响。

③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

MySQL中,可以使用连接( JOIN )查询来替代子查询。 连接查询 不需要建立临时表 ,其 速度比子查询

要快 ,如果查询中使用索引的话,性能就会更好。

排序优化

排序优化

小结


GROUP BY优化

优化分页查询

优化思路一

在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a
WHERE t.id = a.id;


优化思路二

该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;


优先考虑覆盖索引(查询的列,全都是二级索引的值,不需要回表)

什么是覆盖索引?

理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了一个索引包含了满足查询结果的数据就叫做覆盖索引。

理解方式二非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。

简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。(不需要回表操作)


覆盖索引可以使一些本来不走索引的变成走索引,原因无别,只要是优化器认为成本低的操作就会选择该操作,因为覆盖索引省去了回表操作。】

如下:


学习技术的方法:

任何事物的出现都应该是有利有弊的,学技术时,先看技术的实际应用是怎么样的,先学会应用,然后如果要学得深,就要不断去问为什么,一直往深去看。就像以前SSH、SSM、微服务等等,每出现新事物都是对旧事物的迭代,相应就会有一些好处,但是也会有一些弊端。因为任何东西都应该是有利有弊的。


覆盖索引的利弊

好处:

1. 避免 Innodb 表进行索引的二次查询(回表)

2. 可以把随机IO变成顺序IO加快查询效率 (回表是随机IO的)

弊端:

索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。


如何给字符串添加索引

有一张教师表,表定义如下:

create table teacher
(
    ID    bigint unsigned primary key,
    email varchar(64), 
  	...
) engine = innodb;

要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:

select col1, col2 from teacher where email='xxx';

如果email这个字段上没有索引,那么这个语句就只能做 全表扫描 。

前缀索引

MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。

mysql> alter table teacher add index index1(email); 
#或
mysql> alter table teacher add index index2(email(6));

这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。

如果使用的是 index1(即email整个字符串的索引结构),执行顺序是这样的:

  1. 从index1索引树找到满足索引值是’ zhangssxyz@xxx.com ’的这条记录,取得ID2的值;

  2. 到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;

  3. 取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=' zhangssxyz@xxx.com ’的条件了,循环结束。

这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。

如果使用的是 index2(即email(6)索引结构),执行顺序是这样的:

  1. 从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;

  2. 到主键上查到主键值是ID1的行,判断出email的值不是’ zhangssxyz@xxx.com ’,这行记录丢弃;

  3. 取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然后判断,这次值对了,将这行记录加入结果集;

  4. 重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。

也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。 前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。

前缀索引对覆盖索引的影响

索引下推(面试会考)

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

简单来说就是,如果筛选条件是联合索引中有的字段,那么就可以先把需要回表的记录数筛选到最少,然后再进行回表操作,减少需要回表操作的次数就是减少随机IO的次数。

使用前后的成本差别

使用前,存储层多返回了需要被index filter过滤掉的整行记录 。

使用ICP后,直接就去掉了不满足index filter条件的记录,省去了他们回表和传递到server层的成本。

ICP的 加速效果 取决于在存储引擎内通过 ICP筛选 掉的数据的比例。

ICP使用案例

案例1

SELECT *
FROM tuser
WHERE NAME LIKE '张%'
  AND age = 10
  AND ismale = 1;

可以 show profile 看一下



ICP的使用条件

ICP 的使用条件:

只能用于二级索引(secondary index) (本质是减少回表操作,没有回表操作就没有意义了,聚簇索引 和 覆盖索引就没有意义了)

②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。

③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。

④ ICP可以用于MyISAM和InnnoDB存储引擎

⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。

⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。


其它查询优化策略(面试会问)

EXISTS 和 IN 的区分

问题:

不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?


COUNT(*)与COUNT(具体字段)效率

问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?

关于SELECT(*)

在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:

① MySQL 在解析的过程中,会通过 查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。

无法使用 覆盖索引


LIMIT 1 对优化的影响

针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。


多使用 COMMIT

只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。

COMMIT 所释放的资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer 中的空间
  • 管理上述 3 种资源中的内部花费

淘宝数据库,主键如何设计的?(面试可以问主键怎么设计,谈见解,注重思考)

聊一个实际问题:淘宝的数据库,主键是如何设计的?

某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显的错误就是关于MySQL的主键设计。

大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错 !

这样的回答,只站在了数据库这一层,而没有 从业务的角度 思考主键。主键就是一个自增ID吗?站在2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到 。


自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:

1. 可靠性不高

存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。

2. 安全性不高

对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。

3. 性能差

自增ID的性能较差,需要在数据库服务器端生成。

4. 交互多

业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。

5. 局部唯一性

最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。


淘宝的主键设计

在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键 淘宝是如何设计的呢?是自增ID吗?

打开淘宝,看一下订单信息:

从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号:

1550672064762308113 
1481195847180308113 
1431156171142308113 
1431146631521308113

订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增的。

大胆猜测,淘宝的订单ID设计应该是:

订单ID = 时间 + 去重字段 + 用户ID后6位尾号

这样的设计能做到全局唯一,且对分布式系统查询及其友好。


推荐的主键设计

非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。

核心业务 :主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。

这里推荐最简单的一种主键设计:UUID。

UUID 的特点:

全局唯一,占用36字节,数据无序,插入性能差。

认识UUID:

为什么UUID是全局唯一的?

为什么UUID占用36个字节?

为什么UUID是无序的?

MySQL数据库的UUID组成如下所示:

UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)

我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:

为什么UUID是全局唯一的?

在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00 到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降 低到1/100ns。

时钟序列是为了避免时钟被回拨导致产生时间重复的可能性MAC地址用于全局唯一

为什么UUID占用36个字节?

UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。

为什么UUID是随机无序的呢?

因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。

改造UUID

若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。 MySQL 8.0可以更换时间低位和 时间高位的存储方式,这样UUID就是有序的UUID了。

MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符 串用二进制类型保存,这样存储空间降低为了16字节。

可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行

转化:

SET @uuid = UUID(); 

SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);

通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要的主键!

4 、有序 UUID 性能测试

16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?

我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:

从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在 业务端就可以生成 。还可以进一步减少SQL的交互次数。

另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。

如果不是MySQL8.0 肿么办?

手动赋值字段做主键!

比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。

可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编号的最大值。

门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当 前会员编号的最大值。

这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进行操作,就解决了各门店添加会员时会员编号冲突的问题。