因为笔者现在工作中用的存储引擎大多是 InnoDB,所以本文基于 InnoDB,数据库版本MySQL 5.7为前提写的。我们平常说的 SQL 优化,基本上就是对索引的优化。这里既然重点是 SQL 优化,所以我们得先了解索引,然后了解下我们分析 SQL 的工具 explain,最后才能到优化。这也是本文的大纲顺序。
创新互联坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站建设、成都网站制作、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的迁安网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
了解 SQL 优化之前,有几个概念需要先知道:
B+Tree,是 M 阶搜索树。现在以主键索引为例,非叶子节点会冗余我们的主键排序并构成树结构(非叶子节点不会存储数据);叶子节点会存储数据,并且叶子节点会形成一个双向链表,值得注意的是首尾节点也有指针互相指向。(具体可以看:https://segmentfault.com/a/1190000008545713?utm_source=sf-related)
叶子节点存储索引对应的 record信息。
叶子节点只存储主键数据,所以要查询索引以外的数据需要回表。
走非聚簇索引得到主键数据后,根据主键再走一次聚簇索引那里查询列需要的数据。
优化器是MySQL 众多组件中的一个,它会对我们的 SQL 进行分析,看预计使用哪些索引,SQL 的执行顺序如何,实际会使用哪些索引(没有真的执行 SQL,执行 SQL 是存储引擎去进行读写的),使用索引的情况等等。
需要知道使用 InnoDB 的表肯定有一个聚簇索引(有且仅有一个),使用的数据结构是 B+Tree。
*.frm:数据表结构相关信息存储的文件
*.idb:索引和数据存储的文件
注意:*.idb 这个文件本身就是 B+Tree 的文件,叶子节点包含完整的数据记录。
下面以主键索引为例(我的user表就只有三个字段)
如果我们没有主键,MySQL会使用我们表从第一列开始选择一列所有元素都不相等的列构建B+Tree,假设我们不存在符合这个要求的列,MySQL会自己为我们创建一个符合这个条件的隐藏列构建索引。像这种开销没必要花费,我们自己建表时,直接处理可以。
维护B+Tree时,更容易,性能更好。
查询范围时,整型比较大小更简单;整型占用空间更小,节约空间,事实上公司一般都会要求明确字段大小,过大字段,DBA一般都会要求开发解释为什么要这么大,当然从存储数据量角度来看,索引也是越小越好。
二级索引是非聚集的,主要是为了节约空间。二级索引是先找到主键,通过主键回表找到真正的数据行。
假如现在我有个用户表有4个字段:username、telephone、age、sex。
我们可以建两种类型的联合索引:联合主键,普通的联合索引。
现在我用 username、sex 构建成联合主键,维护索引如下:
这个和上面的差不多,只是 data 存的是主键,需要回表查找。
以上图为例子,先根据名字转成的ascii码进行排序,如果 ascii 码一样,那么再根据性别的 ascii 码大小比较排序。只有 username 的索引生效了,sex 的索引才有可能生效。要证明也很容易:如果没有匹配 username,直接匹配 sex,单看 sex 的话,我们索引的排序是无序的,就没法使用二分法了,所以不走索引。
讲了索引的数据结构,以及生效的情况,那么接下来就要看看如何 SQL 优化了。但是在此之前,我们要先了解下 explain 。
使用 explain 可以模拟优化器执行 SQL,分析 SQL,看看能否优化。
explain 标识的 SQL 不会真的执行,只是返回执行计划。如果 from 中包含子查询,仍会执行该子查询,子查询的结果将会放在临时表中。
explain 分析的 SQL 中,每查询一个表就会有一行记录。
更多内容请参考官方文档:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
了解每一列的意义,掌握最常用那几列。
id 列的编号是 select 的序列号,查几个表就有几个 id,并且 id 值越大执行优先级越高。如果 id 值相同,就从上往下执行,最后执行 id 为 null 的。
查询类型。
简单查询。查询不包含子查询和union。
复杂查询中最外层的 select。
包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义)。如下:
#关闭mysql5.7新特性对衍生表的合并优化
set session optimizer_switch='derived_merge=off';
explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der;
• union
其实就是使用了 union 关键字后面的查询,如下:
表示这一列使用的是哪一张表。
当 from 子句中有子查询时,table列是格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。如下图:
当有 union 时,UNION RESULT 的 table 列的值为,1和2表示参与 union 的 select 行id。
使用的哪个分区,需要结合表分区才可以看到。因为我的例子都是没有分区的,所以是 null。
关联类型或者访问类型。一般要保证查询达到 range 级别,最好达到 ref。
从最优到最差:system > const > eq_ref > ref > range > index > ALL。
const 是 MySQL 能对查询的某部分转成一个常量,如下:
而 system 是 conts 的一个特例,当表里只有一条记录时,匹配时为 system。
使用了主键字段或者唯一索引字段进行关联,最多只会返回一条符合条件的记录时,等级为 eq_ref。
explain select * from film_actor left join film on film_actor.film_id = film.id
相较于 eq_ref,它使用的是普通索引或者唯一索引的部分前缀,可能会找到多条符合条件的记录。
范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。
explain select * from actor where id > 1;
这种一般是通过扫描某个二级索引的所有叶子节点(其实就是应该做全表扫描,但是这里利用了B+Tree的叶子节点是链表的特性遍历)。这种方式,虽然比较慢,但是用覆盖索引优化,性能上还是要比全表扫描(ALL)要好的,因为它占用空间小,一次IO可以读更多数据。
这个级别没啥好说的,就是我们常说的全表扫描。
显示可能会使用的索引。
实际会使用的索引。
通过这个值,可以推算出使用到索引的哪些列(一般针对联合索引使用多些),举个例子:
film_actor 的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个 int 列组成,并且每个 int 是4字节。通过结果中的 key_len=4 可推断出查询使用了第一个列:film_id列来执行索引查找。
explain select * from film_actor where film_id = 2;
key_len计算规则如下:
– char(n):如果存汉字长度就是 3n 字节
– varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串
– tinyint:1字节
– smallint:2字节
– int:4字节
– bigint:8字节
– date:3字节
– timestamp:4字节
– datetime:8字节
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)。
这一列是MySQL估计要读取并检测的行数,注意这个不是结果集里的行数。
通过过滤条件之后对比总数的百分比。
这一列展示的是额外信息。常见的重要值如下:
使用覆盖索引。覆盖索引其实就是查询列是索引字段,这样就能避免回表,提高性能。因此,我们覆盖索引针对的是辅助索引。
使用 where 语句处理结果,并且查询列未被索引覆盖。如下:
explain select * from actor where name = 'a';
查询的列没被索引完全覆盖, where 条件中是一个前导列的范围。
explain select * from film_actor where film_id > 1;
创建临时表来处理查询
(1)actor.name没有索引,此时创建了张临时表来distinct。
explain select distinct name from actor;
(2)film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表。
explain select distinct name from film;
使用外部排序而不是索引排序,数据量较小时使用内存,否则会使用磁盘。
(1)actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录。
explain select * from actor order by name;
(2)film.name建立了idx_name索引,此时查询时extra是using index。
explain select * from film order by name;
Using filesort 原理详解:
– 单路排序
一次性取出满足条件的所有字段,然后在 sort buffer 中排序。用 trace 工具可以看到 sort_mode 信息里显示 或者 < sort_key, packed_additional_fields>
– 双路排序(回表排序)
先根据条件获取相应的排序字段和可以直接定位行数据的行ID,然后在 sort buffer 中排序,最后回表获取完整记录。用 trace 工具可以看到 sort_mode 信息里显示 。
– MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来判断使用哪种排序模式。
使用某些聚合函数(比如 max、min)来访问存在索引的某个字段时为 Select tables optimized away。
极端点说,SQL 优化就是对索引的优化。因此,我们要看下各种情况下,如何优化索引。
在我看来,SQL优化分以下几种情况:
1.可以走索引
2.没法走索引(客观现实上的)或者 type 是 index,而且数据量大
3.小表驱动大表
4.count 查询优化
5.如何建索引
Note : 单个索引生不生效,怎么处理还是比较简单的,所以下面只针对联合索引做分析。
下面先建表和造数据:
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100004 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='演员表';
CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='电影表';
CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='电影演员中间表';
其中员工表插入了10W+数据。
explain SELECT * FROM `employees` where `name` like "sai%" and age = 22 and position = 'dev';
当然我们也可以选择强制走索引,如下:
explain SELECT * FROM `employees` force index(idx_name_age_position) where `name` like "sai%" and age = 22 and position = 'dev';
不过,走索引一定性能就更好吗?我们试验下。
-- 关闭查询缓存
SET GLOBAL query_cache_size = 0;
SET GLOBAL query_cache_type = 0;
-- 耗时 0.064s
SELECT * FROM `employees` WHERE `name` LIKE "sai%" AND age = 22 AND position = 'dev';
-- 耗时 0.079s
SELECT * FROM `employees` force index(idx_name_age_position) where `name` like "sai%" and age = 22 and position = 'dev';
别看我这差距不大,我这只是表列不多,字段不大,数据量也不算太多,所以差距不大,如果表更大的话,差距就会比较明显了。实际工作中,我们很难确定走索引的 cost 就一定小于全表扫描的。因此,我们一般不强制走索引。
优化方案:
我想让 MySQL自己去走索引,而不是我强制走索引。怎么办呢?其实上面已经提到了,这里是因为第一个字段过滤不多,导致回表效率低。既然如此,我们让它不回表不就好了吗?使用覆盖索引优化,就是我们查询列的字段都是使用的这个索引树上建了索引的字段,这样就不需要回表了。如下:
explain SELECT id,`name`,age,position FROM `employees` where `name` like "sai%" and age = 22 and position = 'dev';
-- 耗时 0.051s
SELECT id,`name`,age,position FROM `employees` where `name` like "sai%" and age = 22 and position = 'dev';
扩展:
SELECT * FROM `employees` WHERE `name` LIKE "sai%" AND age = 22 AND position = 'dev';
分页查询,系统十分常见的查询,建议大家学习完后,赶紧看下自己负责的分页功能是否走索引了,或者是否走了索引但是还能优化。以下,看例子来说一些优化手段。
select * from employees limit 10000, 10;
这 SQL 其实是去了10010条记录出来,然后再舍弃前面的一万条。因此数据量大的话,其实效率是十分低的。
一些优化方案:
1.和产品同事商量,给一些一定有的查询条件或者隐藏的查询条件,给这些条件使用上索引。
这个方案是最简单并且直接的。
2.像我这里记录的id是连续且自增的情况下:
explain select * from employees where id > 10000 limit 10;
属于取巧,通过主键索引使用 where 直接筛选掉前面10000条记录。
缺点:
(1) 如果 id 不是连续且自增,那么这种方式就不行。
(2)不是使用主键排序,这种情况也不行。
3.非主键排序,不用ID连续自增也能生效。
-- 0.085s
select * from employees order by `name` desc limit 10000, 10;
explain select `name`, age, position from employees order by `name` desc limit 10000, 10;
-- 0.077s
select `name`, age, position from employees order by `name` desc limit 10000, 10;
扩展:
-- 我们常认为 like 以通配符开头,索引会失效,但其实也可以通过覆盖索引,让索引生效。
explain select `name`, age, position from employees where `name` like '%sai%';
解决方案如下:
(1)
explain select e.* from employees e inner join (select id from employees order by `name` desc limit 10000, 10) t on t.id = e.id;
-- 0.045s
select e.* from employees e inner join (select id from employees order by `name` desc limit 10000, 10) t on t.id = e.id;
这里其实就是利用了二级索引,拿到了10010条数据,并且按照 name 排好序,由于这里的子查询只要 id,所以不需要回表,然后再通过 join 就能利用主键索引快速拿到记录。
(2)当然除了这种方式,我们也可以强制走索引,因为我们知道这里二级索引只有一个,并且 name 是前导列,所以我这个案例走索引性能肯定比全表扫描好。因此,我们也可以选择强制走索引。
-- 0.011s
select * from employees force index(idx_name_age_position) order by `name` desc limit 10000, 10;
我们索引之所以可以帮我们快速找到目标数据,是因为它的数据结构的特点。其中有序这一特征十分重要,如果不满足,那么肯定是不会走索引的(具体原因要回到平衡二叉查找树,再到二分法。因为不是这里的重点,所以不展开讲)。
– 对索引列是用了函数
– 对索引列做了类型转换
-- 类型转换会有特例,当我们转成日期范围查询时,有可能走索引。
ALTER TABLE `employees` ADD INDEX `idx_hire_time` (`hire_time`) USING BTREE ;
EXPLAIN select * from employees where hire_time >='2018‐09‐30 00:00:00' and hire_time <
='2018‐09‐30 23:59:59';
其实第一个案例已经涉及到了,但是这里针对的是不等于, not in, not exists, <, >, is null, is not null 等等,这些能匹配到多条记录的写法。
排序和分组的优化其实是十分像的,本质是先排序后分组,遵循索引创建顺序的最左匹配原则。因此,这里以排序为例。
https://www.cnblogs.com/25-lH/p/11010095.html这个博客有讲到无查询条件的排序的案例,我这里就直接上图了,如下:
接下来写的都是有查询条件的情况。
explain select * from employees where `name` = 'sai999' and position = 'dev' order by age;
-- 这里没有走索引,是因为不符合最左原则,跳过了 age
explain select * from employees where `name` = 'sai999' order by position;
-- 这样就会走索引了,排序了
explain select * from employees where `name` = 'sai999' order by age, 1position;
-- 又不走索引了,因为 age 和 position 顺序颠倒了,不符合我们索引的顺序
explain select * from employees where `name` = 'sai999' order by position, age;
-- 修改成这样,就又可以走索引了,因为 age 是个常量了,所以在排序中被优化,没有和索引顺序冲突
explain select * from employees where `name` = 'sai999' and age = 999 order by position, age;
-- 这里虽然符合索引顺序,但是 age 是升序,而 position 是降序,所以不走索引。听说 MySQL 8 支持这种查询方式,我没安装8就不测试了
explain select * from employees where `name` = 'sai999' order by age asc, position desc;
-- 想想我们联合索引的 B+Tree 数据结构,当 name 有两个值时,得出的结果集对于 age, position 而言是无序的,所以没法走索引
explain select * from employees where `name` in ('sai100', 'sai101') order by age, position;
-- 可以使用覆盖索引优化
explain select `name`, age, position from employees where `name` > 'a' order by `name`;
MySQL 支持两种排序方式 filesort 和 index, Using index 是扫描索引完成的排序,而 Using filesort 是利用内存甚至磁盘完成排序的。因此,index 效率高,filesort 效率低。
当我们做多表关联查询时,常常会听到小表驱动大表。这里要了解什么是小表,什么是大表,为什么是小表驱动大表,MySQL 用了什么算法。
下面以两张表关联为例,介绍概念
什么是小表,什么是大表?不是表数据量较多那张表就是大表!!!而是经过我们的条件筛选后,匹配数据相对较小的那张表就是小表,另外一张就是大表。
所谓的小表驱动大表就是:先查小表,然后通过关联字段去匹配大表数据。
MySQL 的表关联常见有两种算法:
这个算法就是一次一行地从驱动表中读取,通过关联字段在被驱动表中取出满足条件的行,然后取出两张表的结果合集。
explain select * from uuc_user u inner join uuc_user_role ur on ur.user_id = u.id;
从执行计划可以看出:
注意:优化器一般会优先选择小表驱动大表,我们 SQL 写的表的先后顺序有可能会被优化。
上面 SQL 的大致流程如下:
查询结果如下(由于数据太多,手工拼接图了)
NLJ这个过程会读取 ur 所有数据(9行记录),每次读一行并拿到 user_id 的值,然后得到对应的 uuc_user 里的记录(这就是又扫了一次索引得到一行数据)。也就是说,整个过程扫描了18行记录。注意:如果被驱动表的关联字段没有索引,使用NLJ算法性能较低,MySQL会选择使用 BNL 算法。
扩展:如果我这里使用的是 left join,这时,左边的是驱动表,右边的是被驱动表;right join 则刚好相反。
explain select * from uuc_user u left join uuc_user_role ur on ur.user_id = u.id;
把驱动表的数据读入 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来和 join_buffer 中的数据做匹配。
上面扩展已经出现了 BNL 算法的例子了,我就直接使用了。
select * from uuc_user u left join uuc_user_role ur on ur.user_id = u.id;
这条 SQL 的流程大致如下:
整个过程扫描了 uuc_user 表225条记录和 uuc_user_role 表9条记录,总扫描行数为234行。内存比较最大次数 = 225 * 9 = 2025(次),想想 for 循环的代码就知道了。
两个问题:
答案:
互联网公司其实一般不允许做多表关联,如果做了关联,最多不超过3张表。多表关联时,关联字段一定要有索引,并且数据类型保持一致。为什么这么要求?直接原因,阿里规范(老大都这样规范,小弟跟着做,没毛病)。根本原因?看《高性能MySQL》,这本书推荐阅读。
-- 像 select * from uuc_user u inner join uuc_user_role ur on ur.user_id = u.id; 这 SQL
-- 我们可以优化成下面的 SQL,用左边的表驱动右边的表
explain select * from uuc_user_role ur straight_join uuc_user u on ur.user_id = u.id;
原则还是小表驱动大表
假设 A 表是左表,B 表是子查询的表。当 A 表是大表, B 表是小表时,使用 in。
select * from A where id in (select id from B)
当 A 表是小表, B 表是大表时,使用 exsits。
-- exists(subquery)只返回 true 或 false,官方也有说过实际执行时会忽略查询列。因此,select * 和 select 1 没区别。
-- exists子查询实际执行过程是被优化了的,不是我们之前理解的逐条匹配。
select * from A where exists (select 1 from B where B.id = A.id)
网上挺多资料说,要count(id)或者count(1),不要count(*),到底是不是这样呢?我们今天就来实践一下。
-- 临时关闭查询缓存,看实验的真实时间
set global query_cache_size=0;
set global query_cache_type=0;
-- 首先下面四条语句得到的执行计划都是一样的,说明理论上这四个SQL的执行效率应该是差不多的
explain select count(1) from employees; -- 有时0.03左右,有时0.015s左右
explain select count(id) from employees;-- 稳定在0.015s左右
explain select count(*) from employees;-- 稳定在0.015s左右
explain select count(`name`) from employees;-- 稳定在0.015s左右
具体耗时如下(其实,随着电脑的状态不同,会有出入,但是多次测试会发现,这截图的排序结果是多数)。
因此,我们可以看出 count(*) 少用,性能较差是谣言,可以放心使用。这是因为 MySQL 5.6+ 会对 count(*) 进行优化,所以执行效率还是很高的。
hire_time 慢的原因是因为没有索引。
老生常谈的东西了,面试也经常问,这里就做个总结。
对于如何建索引这个问题,我个人觉得应该从以下几个角度思考:
索引的数量要尽量的少。
1.代码先行,索引后上
只有对系统有了一定全局观,才知道哪些地方需要用索引,大多 SQL 是怎样的,我应该如何建索引。这样,我们就能有效减少不必要的索引,做到联合索引尽量覆盖条件。
2.尽量不要在过滤数据不多的字段建立索引,如:性别。
3.where 与 order by 冲突时,优先处理 where。
蔡柱梁,社区编辑,从事Java后端开发8年,做过传统项目广电BOSS系统,后投身互联网电商,负责过订单,TMS,中间件等。
网站栏目:MySQL数据库的SQL调优,你会了吗?
网站链接:http://www.shufengxianlan.com/qtweb/news29/120879.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联