Mysql 如何让你的查询速度更快?

准备工作

dev.mysql.com/doc/index-o…

首先去上述地址 下载sakila 数据库。 这是准备好的测试数据

where 查询慢?

show create table inventory
复制代码

先查看这个表的 建表语句

我们就拿到了这个表的sql 语句

CREATE TABLE `inventory` (
  `inventory_id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `film_id` smallint(5) unsigned NOT NULL,
  `store_id` tinyint(3) unsigned NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`inventory_id`),
  KEY `idx_fk_film_id` (`film_id`),
  KEY `idx_store_id_film_id` (`store_id`,`film_id`),
  CONSTRAINT `fk_inventory_film` FOREIGN KEY (`film_id`) REFERENCES `film` (`film_id`) ON UPDATE CASCADE,
  CONSTRAINT `fk_inventory_store` FOREIGN KEY (`store_id`) REFERENCES `store` (`store_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4582 DEFAULT CHARSET=utf8mb4
复制代码

我们修改一下这个sql语句 去掉 idx_store_id_film_id 这个联合索引 以及 对应的2个 外键

CREATE TABLE `inventory_1` (
  `inventory_id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `film_id` smallint(5) unsigned NOT NULL,
  `store_id` tinyint(3) unsigned NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`inventory_id`),
  KEY `idx_fk_film_id` (`film_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4582 DEFAULT CHARSET=utf8mb4
复制代码

如此一来,这里就有一个新表了

然后我们把老表的数据 拷贝一份 到新表中

insert into inventory_1 select * from inventory
复制代码

查询下新表 image.png

标注出来的索引都是null 显示没有使用过

再看下老表 image.png

末尾显示了using index,就代表了使用了索引

再看小 新表老表 索引的区别 新表: image.png 老表: image.png

再看个例子:

image.png

这里可能有人觉得奇怪了,这个例子查询了3个字段, 为啥还是可以是索引查询呢?为啥没有回表?因为这里的查询 所出来的字段 本身就是一个主键, 我们联合索引查询的结果 就是主键,这个在上一篇文章 讲 innodb时有提过,所以这里不需要回表了,

再看一个例子: image.png

和前面的相比 他就是多了一个 lastupate 字段,而且显示查询过程中 也使用了索引,但是 extra 显示的null

意味着 发生了回表了, 因为使用完索引以后 发现 你还需要lastupdate 字段, 所以只能根据查询到的主键结果 再次去表中 重新查询一遍 就是为了取这个字段

为啥不走更合适的索引?

Mysql 在选取索引的时候,会参考索引的基数,这个基数是mysql 估算出来的,反应这个字段有多少种取值。

一般来说,选取几个页 算出取值的平均值,再乘以页数,

可以看下 下面这个实验

建一个简单的表

create table sakila.city_1(city varchar(50) not null);
复制代码

然后 把老表的数据 导入进来,多导入几次

insert into sakila.city_1 select city from sakila.city;
insert into sakila.city_1 select city from sakila.city;
insert into sakila.city_1 select city from sakila.city;
insert into sakila.city_1 select city from sakila.city;
insert into sakila.city_1 select city from sakila.city;
复制代码

最后 我们要把这个表的内容的数据打乱,

update sakila.city_1 set city=(select city from sakila.city order by rand() limit 1);
复制代码

image.png

然后对这个表增加索引

alter table sakila.city_1 add key (city(1));
alter table sakila.city_1 add key (city(2));
alter table sakila.city_1 add key (city(3));
alter table sakila.city_1 add key (city(4));
alter table sakila.city_1 add key (city(5));


alter table sakila.city_1 add key (city(6));
alter table sakila.city_1 add key (city(7));
alter table sakila.city_1 add key (city(8));
复制代码

然后看一下 索引的情况吧

image.png

右边的card列,就代表mysql 估算出来的基数了, 第一个city 取第一个字母为索引的,这个对应的值是26

这个很准对吧,因为总共就26个字母。

然后 后面的第三个 第四个 准确度也可以,再后面的索引 准确度就不太行了。

这是为啥? 因为前面说过了,这个是mysql 自己估算出来的,肯定不够精确,所以有的时候你会觉得奇怪, 为啥mysql 没有使用你觉得理想中的索引 就是这个原因。

要解决这个问题 很简单,你可以使用 force index 来强制使用索引。

也可以 analyze table 重新统计索引信息的

实际上 根据索引基数,就可以判断索引性能的好坏了。

count 这么慢,咋办呢?

这个count函数 用来统计结果集中 不为null的数据个数。

执行过程如下:

首先 查出结果集

然后 逐个判断 是否为null 不为null 则 +1

可以看下 下面的customer 表 看下字段和索引

image.png

看下执行过程

image.png

这个执行过程就很慢了,没有使用任何索引, 是走的 全表扫描

再看看这个

image.png

这个执行过程就明显是 使用了索引了,但是 他依旧需要 每次都判断这个last_name 是否为null

下面有人就说了,那我主键 不可能为null吧,这个也是查询是否为null吗?

是的,mysql 就是这样处理的,没有对主建的 count进行优化,虽然主键 不可能为null,但是他依旧会在count的时候 判断是否为null

 select count(customer_id) from customer;
复制代码

有人觉的 count(1) 是不是会更快?

select count(1) from customer;
复制代码

理论上count 1 是会更快一点,但是快的原因是 查询过程中 缺少了 解析数据行的操作,所以快了一些,但是遍历判断null的过程 依旧没有省略

select count(*) mysql 其实就是对这个做了优化

如果是MyISAM 存储引擎,那 count * 直接返回数据表行数

如果是Innodb,虽然数据库中没有记录数据表行数,但是mysql 在这里 专门做了处理,直接返回索引数中数据的个数

所以大家一般情况下 无脑 count(*) 即可

order by 太慢 咋办呢?

select * from film where film_id>80 order by title
复制代码

mysql 对上述语句的执行结果如下:

根据where 条件查询出结果

将结果 放入 sort_buffer

对中间结果集 按照 order 字段排序

**如果有需要的话 要回表生成完整结果集 **

还是前面那条sql 语句 怎么对他进行优化?

image.png

film id 已经是主建了。已经是索引优化的查询了。

优化方向 1:

我们根据where 查询出来的结果 如果比较小,那么就会把这个结果 直接放到 sort buffer 中,这是个内存buffer

如果查询出来的结果比较大,那么就会把这个结果集 放到 硬盘中了。 那显然 放到硬盘中 会涉及到io 操作 这个就比较慢了。

所以我们可以 调整 sort_buffer_size的值 将这个值放大 这样就会减少 放到硬盘中的概率了。但是这样会增加内存的占用。 这里要综合考虑。

优化内存占用和优化排序查询时间 两者往往不可兼得

查询过程中的回表:

假设你的表有100个字段,那么当where查询出来以后的结果集 mysql 会根据 max_length_for_sort_data

这个值的大小来确定 这个中间表为多大,假设 中间表只有30个字段,那么显然排好序以后 还要回一次表,去拿

全部的数据。

有人就说了,我们把这个max_length_for_sort_data 字段调大 不就可以防止回表了吗?因为结果集是一张

完整的表啊,这个说法是错误的,因为这样会导致结果集太大,很有可能这个表就会放到硬盘中 而不是 内存中了

那如何对上述的 查询优化呢? 关键词就是 索引覆盖

索引覆盖这个东西 可以直接跳过生成中间结果集,直接输出查询结果

那如何达成索引覆盖的 条件呢?

  1. order字段必须要有索引(或者是联合索引的左侧)

2/ 其他相关的字段(查询条件,结果集) 也必须在上述的索引中

比如:

select film_id,title from film order by title
复制代码

这条语句 就是索引覆盖

再比如说:

 select title,film_id from film where title like 'm%' order by title;
复制代码

这个也是索引覆盖,因为 筛选字段,排序字段,输出字段 都是同一个索引 title

rand 函数 太慢 咋办?

select title,description from film order by rand() limit 1;
复制代码

首先解释一下这条语句

就是从film表里面 选出title和des 2个字段 然后对这些数据 进行 随机排序,并且取其中的 第一条数据

看下 上述语句的 执行过程:

  1. 创建一个临时表,字段为 rand title 和 des
  2. 从表中取一行,调用rand()函数,将结果放到临时表

3.针对临时表,将rand字段和临时表的 行位置或者主键 放入sort_buffer(这里就是第二张临时表了)

  1. 对 sort buffer排序,取出第一个行位置,再回去查临时表

这个执行过程,看起来就很慢的样子,因为 执行过程中 竟然涉及到 2个临时表,而且临时表都是全长度的

其次,仅仅需要一个随机结果,却经历了不必要的排序(虽然mysql 会自动优化)

最后 还调用了很多次rand() 函数

那 看看怎么优化上述的查询

  1. 查询数据表总数 total (count *)
  2. total 范围内 随机选取一个数字 r
  3. 执行下面的 sql

select title,description from film limit r,1

使用上述方案 就可以大幅提高 查询速度了。

一般情况下 不要使用 order by rand() limit 1. 这样的随机查询效率太低

索引下推

create table `inventory_3`(
    `inventory_id` mediumint unsigned not null auto_increment,
    `film_id` smallint unsigned not null ,
    `store_id` tinyint unsigned not null ,
    `last_update` timestamp not null default current_timestamp on update current_timestamp ,
    primary key (`inventory_id`),
    key `idx_store_id_film_id` (`store_id`,`film_id`)
) ENGINE =InnoDB auto_increment=101 default charset =utf8;

insert into inventory_3 select * from inventory
复制代码

image.png

然后看下面这个查询语句

select * from `inventory_3` where store_id in(1,2) and film_id=3
复制代码

有人觉得 ,这里有联合索引了,这条查询肯定是很快的一次索引结果就结束了吧,

但其实并不是。

为啥?

你看这个联合索引的 结构

store_id 最左边吗,film_id 在 sid的右边, 这俩索引的查询结果 就是 主键

当你使用这个联合索引的时候

先查store id 如果store id 相等,再去 film id 里面查, 这俩列 都是排序好的

但是 如果 你是 in函数

他查询的过程就是:

store id 等于1 或者等于2 的结果,注意了,这个结果对应的film id 是没有排序的 所以,film id 的查询 就不是索引查询了。。

image.png

他的查询过程就是:

using index condition 就代表索引下推了

image.png

再看下 正常索引查询:

image.png

实际上 mysql 5.6 之前,回表会非常频繁,在这之后 实际上有优化的,所以上述2个 rows的 次数差别并不大

有兴趣的可以看下 5.5的mysql rows的行数差距

另外就是

select film_id from inventory_3 where film_id=3

这个sql语句 大家前面的看过 应该知道,这里不会有索引查找的效果,因为 store_id 才是索引的左边

但是在mysql 8.0中, 这个会支持 松散索引扫描, 可以大大的减少查询耗时,比之前的mysql版本 都要快很多

为啥我的索引不走了?

explain select * from film where film_id+1=100
复制代码

比如这个查询, 因为对索引字段做了函数操作,所以优化器 会放弃索引

因为 我们是对film_id 做的索引,不是对film_id +1 做的索引

有人会说,你这个sql语句 太奇葩了,现实中 不会有人这么写的

看下面这个租约表

image.png

我们想查询 5月份的 租约 信息

select * from rental where month(rental_date)=5
复制代码

注意 rental_date 是有索引的

看下执行: image.png

你看 rows这么多行,完全没有索引的效果吧

使用了 month函数 就无法使用索引了

那有人说 这个sql 语句咋写呢?我就非要用索引

可以用bewtten 来优化

select * from rental where rental_date between '2021-5-1' and '2021-6-1'
复制代码

当然你可以后面 拼接 or 关键字, 来把去年的 5月份 也找出来。

select * from t1 where f1=6
复制代码

字符串与数字比较,会将字符串 转换为数字

因为f1 字段 为varchar类型,所以 上述的sql 等于

select * from t1 where cast(f1 as signed int)=6
复制代码

所以 我们要尽量避免这种情况。

select * from t1 where f1='6'
复制代码

再看

建表语句如下:

create table `t1`(
    `f1` varchar(32) not null ,
    `f2` int not null ,
    key `idx_f1` (`f1`),
    key `idx_f2` (`f2`)
)engine =InnoDb  default charset = utf8;

create table `t2`(
    `f1` varchar(32) not null ,
    `f2` int not null ,
    key `idx_f1` (`f1`),
    key `idx_f2` (`f2`)
)engine =InnoDb  default charset = utf8mb4;
复制代码

然后用下面这条sql 试试看:

select t2.* from t1,t2 where t1.f1=t2.f1 and t1.f2=6
复制代码

t1.f2=6 这个查询 是使用索引的

但是t1.f1=t2.f1 这个 却没有

类似于这种也要小心, 因为utf8与 utf8mb4 在查询的时候 也是有隐式的字符编码转换的, 这也会导致索引失效。

用这种convert 显示的转一下 即可(当然更推荐的就是 规定好 utf8mb4 这种字符集)

select t2.* from t1,t2 where convert(t1.f1 using utf8mb4)=t2.f1 and t1.f2=6
复制代码

分页查询效率低,咋办?

image.png

看上述的sql语句,实际上这是先执行左边的查询 然后再执行右边方框里面的 limit 语句

可以感知到 这会丢弃掉很多数据,效率很低。

那 如何优化?

看下索引:

image.png

这里很明显可以看到, title 是索引,film id 是主索引,有个des字段 不是索引,那优化上面的sql

就可以进行为

设置 一个联合索引 为title 和des 字段 即可

那么还有没有其他办法 可以优化上面的sql语句?

select f.film_id,f.title,f.description from film f inner join(select film_id from film order by title limit 900,10
) m on f.film_id= m.film_id
复制代码

其实就是 先得到所需数据的主键

原表与上面的结果连表 即可。

猜你喜欢

转载自juejin.im/post/7048152511605112845
今日推荐