MySQL优化(上)2020-08-17

索引优化

索引分类

image.png

回表

假设我们执行一条查询语句
select * from person where ID = 6,因为我们直接使用的主键ID查询,所以就会用主键索引,由于主键索引直接关联了整行数据,所以,引擎只要执行一次就能查询结果

如果执行的结果是非主键索引

select * from person where age=18;

上述语句会走age的普通索引,所以先根据age搜索等于18的索引记录,找到ID=10的记录,然后再到主键索引搜索一次,然后拿出需要查询的数据。

从普通索引查出主键索引,然后查询出数据的过程叫做回表。由于回表需要多执行一次查询,这也是为什么主键索引要比普通索引要快的原因,所以,我们要尽量使用主键索引。

覆盖索引

我们通常创建索引的依据都是根据查询的where条件,但这只是我们通常的做法,我们根据上面的分析可以知道,如果要想查询效率高,第一,使用主键索引,第二,避免回表,也就是在索引中就能获得想要的数据。如果一个索引中包含了我们需要查询的字段,那么我们就叫做覆盖索引

建表SQL

create table staffs(
    id int primary key auto_increment,
    name varchar(24) not null default "",
    age int not null default 0,
    pos varchar(20) not null default "",
    add_time timestamp not null default CURRENT_TIMESTAMP 
)charset utf8;
create table user(
    id int not null auto_increment primary key,
    name varchar(20) default null,
    age int default null,
    email varchar(20) default null
) engine=innodb default charset=utf8;
插入数据
insert into staffs(`name`,`age`,`pos`,`add_time`) values('z3',22,'manager',now());
insert into staffs(`name`,`age`,`pos`,`add_time`) values('July',23,'dev',now());
insert into staffs(`name`,`age`,`pos`,`add_time`) values('2000',23,'dev',now());
insert into user(name,age,email) values('1aa1',21,'[email protected]');
insert into user(name,age,email) values('2aa2',22,'[email protected]');
insert into user(name,age,email) values('3aa3',23,'[email protected]');
insert into user(name,age,email) values('4aa4',25,'[email protected]');
建立复合索引
create index idx_staffs_nameAgePos on staffs(name,age,pos);

口诀

全值匹配我最爱,最左前缀要遵守
带头大哥不能死,中间兄弟不能断
索引列上少计算,范围之后全失效
like百分写最右,覆盖索引不写星
不等非空还有or,索引失效要少用
varchar引号不可丢,SQL高级也不难

全值匹配我最爱
explain select * from staffs where name = 'july' and age =18 and pos = 'dev';
我们查询的条件就是我们建立的索引,刚好全用上了,这样就是比较好的,对于SQL语句来说
最左前缀要遵守(查询索引,从最左前列开始,并且不跳过索引中的列)
explain select * from staffs where name = 'july' and pos = 'dev';
explain select * from staffs where pos = 'dev' and name = 'july';
此时只用到了一个索引列,就是name
这个地方就是从最左前列name开始的,但是跳过了索引列age,所以pos索引列就没用到
name age pos 的索引条件可以随意更换位置,只要存在就可以
带头大哥不能死
explain select * from staffs where age = 18 and pos = 'dev';
此时就没有用到索引,因为没有用到name索引列,带头大哥死了
中间兄弟不能断
explain select * from staffs where name = 'july' and pos = 'dev';
中间兄弟age没有写,直接写了pos,所以这个地方就只用到了一个索引列就是age
索引列上少计算
explain select * from staffs where lower(name)='july';(没有用到索引)
explain select * from staffs where name = 'july' and age-1 = 18;(用到一个索引列就是name)
explain select * from staffs where name = 'july' and age = 18-1;(用到两个索引列,也就是name,age)
索引列上不要计算,索引后面可以计算
范围之后全失效
explain select * from staffs where name = 'july' and age > 18;(用到两个索引列,name  age)
explain select * from staffs where name = 'july' and age > 18 and pos = 'dev';(用到两个索引列 name age  范围后的索引失效了)
explain select * from staffs where name = 'july' and pos = 'dec' and age > 18;(用到两个索引列 name age)
我们这里范围所在的位置与我们写的SQL语句无关,与定义的索引列的位置有关
like百分写最右
explain select * from staffs where name like '%july%';(没有用到索引)
explain select * from staffs where name like '%july';(没有用到索引)
explain select * from staffs where name like 'july%';(用到了索引)
like优化
create index idx_user_nameage on user(name,age);
explain select * from user where name like '%aa%';(没有用到索引)
explain select id from user where name like '%aa%';(用到了索引)
explain select name from user where name like '%aa%';(用到了索引)
explain select age from user where name like '%aa%';(用到了索引)
explain select email from user where name like '%aa%';(没有用到索引)
explain select id,email from user where name like '%aa%';(没有用到索引)
如果查询的字段是索引字段,那么就会用到索引
如果查询的是非索引字段,那么就不会用到索引
如果查询的是索引和非索引字段的组合,那么也不会用到索引
覆盖索引不写星
覆盖索引不写星,当数据量很多的时候,我们用什么取什么,尽量不要写*,影响服务器性能
不等非空还有or,索引失效要少用
explain select * from staffs where name!='july';(没有用到索引)
explain select name from staffs where name!='july';(用到了索引)
explain select * from staffs where name='july' and age<18 or age>18;(没有用到索引,or会使索引失效)
explain select * from staffs where name = 'july' and (age>18 or age<18);(没有用到索引)
explain select * from staffs where name is not null;(没有用到索引)
explain select * from staffs where name is null;(用到了索引)
varchar引号不可丢
explain select * from staffs where name = 2000;(没有使用到索引,但也可以查询到结果)
explain select * from staffs where name = '2000';(使用到了索引)
image.png

索引优化案例

单表优化

  • 建表
create table article(
    id int unsigned not null primary key auto_increment,
    author_id int unsigned not null,
    category_id int unsigned not null,
    views int unsigned not null,
    comments int unsigned not null,
    title varchar(255) not null,
    content text not null
);
  • 插入数据
insert into article(`author_id`,`category_id`,`views`,`comments`,`title`,`content`) values 
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(1,1,3,3,'3','3');
  • 需求:查询category_id为1且comments大于1的情况下,views最多的article_id
create index idx_article_cv on article(category_id,views);
explain select * from article where category_id=1 and comments>1 order by views desc limit 1;

双表优化

  • 建表
    商品类别表
create table class(
    id int unsigned not null primary key auto_increment,
    card int unsigned not null
);

图书表

create table book(
    bookid int unsigned not null auto_increment primary key,
    card int unsigned not null
);

驱动表的概念,mysql中指定了连接条件时,满足查询条件的记录行数少的表为驱动表;如未指明查询条件,则扫描行数少的为驱动表。mysql优化器就是以小表驱动大表的方式来决定执行顺序的。

create index idx_book_card on book(card);
explain select * from class left join book on class.card=book.card;
(左连接往右表中添加索引,右连接往左表中添加索引)
(如果两个表都可以添加索引的话,可以把关联字段添加为索引)

join语句优化

我们在使用数据库查询数据时,有时一张表并不能满足我们的需求,很多时候都涉及到多张表的连接查询。今天,我们就一起研究关联查询的一些优化技巧。在说关联查询优化之前,我们先看下跟关联查询有关的几个算法:

为了方便理解,首先创建测试表并写入测试数据,语句如下:

CREATE TABLE `test_join` ( /* 创建表t1 */
`id` int(11) NOT NULL auto_increment,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
COMMENT '记录更新时间',
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关联查询的算法

  • Nested-Loop Join 算法
  • Block Nested-Loop Join 算法
Nested-Loop Join 算法

一个简单的Nested-Loop Join 算法(NLJ)算法一次一次循环的从第一张表(称为驱动表)中读取行,在这行中去到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。

我们试想一下,如果被驱动表中这个关联字段没有建立索引,那么每次取出驱动表的关联字段在被驱动表查找对应的数据时,都会对被驱动表进行一次全表扫描,成本是非常高的,(比如驱动表数量是m,被驱动表数量是n,则扫描行数是m*n)

好在 MySQL 在关联字段有索引时,才会使用 NLJ,如果没索引,就会使用 Block Nested-Loop Join。我们先来看下在有索引情况的情况下,使用 Nested-Loop Join 的场景(称为:Index Nested-Loop Join)。

因为 MySQL 在关联字段有索引时,才会使用 NLJ,因此本节后面的内容所用到的 NLJ 都表示 Index Nested-Loop Join。

select * from t1 inner join t2 on t1.a = t2.a;

怎么确定这条 SQL 使用的是 NLJ 算法?


image.png

从执行计划中可以看到这些信息:

  • 驱动表是 t2,被驱动表是 t1。原因是:explain 分析 join 语句时,在第一行的就是驱动表;选择 t2 做驱动表的原因:如果没固定连接方式优化器会优先选择小表做驱动表。所以使用 inner join 时,前面的表并不一定就是驱动表。
  • 使用了 NLJ。原因是:一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer (***);则表示使用的 join 算法是 NLJ。

我们再看下 sql2 的执行流程:

  • 把 t2 的所有数据放入到 join_buffer 中
  • 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比
  • 返回满足 join 条件的数据

在这个过程中,对表 t1 和 t2 都做了一次全表扫描,因此扫描的总行数为10000(表 t1 的数据总量) + 100(表 t2 的数据总量) = 10100。并且 join_buffer 里的数据是无序的,因此对表 t1 中的每一行,都要做 100 次判断,所以内存中的判断次数是 100 * 10000= 100 万次。

下面我们来回答上面提出的一个问题:

如果被驱动表的关联字段没索引,为什么会选择使用 BNL 算法而不继续使用 Nested-Loop Join 呢?

在被驱动表的关联字段没索引的情况下,比如 sql2:

如果使用 Nested-Loop Join,那么扫描行数为 100 * 10000 = 100万次,这个是磁盘扫描。

如果使用 BNL,那么磁盘扫描是 100 + 10000=10100 次,在内存中判断 100 * 10000 = 100万次。

显然后者磁盘扫描的次数少很多,因此是更优的选择。因此对于 MySQL 的关联查询,如果被驱动表的关联字段没索引,会使用 BNL 算法。

优化关联查询

关联字段添加索引

通过上面的内容,我们知道了 BNL、NLJ的原理,因此让 BNL变成 NLJ ,可以提高 join 的效率。我们来看下面的例子

我们构造出两个算法对应的例子:

Block Nested-Loop Join 的例子:

select * from t1 join t2 on t1.b= t2.b;
create index idx_t2_b on t2(b);
explain select * from t1 join t2 on t1.b=t2.b;

Index Nested-Loop Join 的例子:

select * from t1 join t2 on t1.a= t2.a;

我们对比下两条 SQL 的执行计划:


image.png

小表驱动大表

前面说到,Index Nested-Loop Join 算法会读取驱动表的所有数据,首先扫描的行数是驱动表的总行数(假设为n),然后遍历这n行数据中关联字段的值,根据驱动表中关联字段的值索引扫描被驱动表中的对应行,这里又会扫描n行,因此整个过程扫描了2n行。当使用 Index Nested-Loop Join 算法时,扫描行数跟驱动表的数量成正比。所以在写SQL时,如果确定被关联字段有索引的情况下,建议用小表做驱动表。

我们来看下以 t2 为驱动表的 SQL

select * from t2 straight_join t1 on t2.a = t1.a;

这里使用 straight_join 可以固定连接方式,让前面的表为驱动表。
再看下以 t1 为驱动表的 SQL:

select * from t1 straight_join t2 on t1.a = t2.a;

我们对比下两条 SQL 的执行计划:


image.png

明显前者扫描的行数少(注意关注 explain 结果的 rows 列),所以建议小表驱动大表。

临时表

多数情况我们可以通过在被驱动表的关联字段上加索引来让 join 使用 NLJ 或者 BKA,但有时因为某条关联查询只是临时查一次,如果再去添加索引可能会浪费资源,那么有什么办法优化呢?

这里提供一种创建临时表的方法。

我们一起测试下:

比如下面这条关联查询:

select * from t1 join t2 on t1.b= t2.b;

我们看下执行计划:


image.png

由于表 t1 和表 t2 的字段 b都没索引,因此使用的是效率比较低的 BNL 算法。

现在用临时表的方法对这条 SQL 进行优化:

首先创建临时表 t1_tmp,表结构与表 t1(被驱动表) 一致,只是在关联字段 b 上添加了索引。

CREATE TEMPORARY TABLE `t1_tmp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_a` (`a`),
  KEY `idx_b` (b)
) ENGINE=InnoDB ;

把 t1 表中的数据写入临时表 t1_tmp 中:

insert into t1_tmp select * from t1;

执行 join 语句:

select * from t1_tmp join t2 on t1_tmp.b= t2.b;

我们再看下执行计划:


image.png

Extra 没出现 “Block Nested Loop”,说明使用的是 Index Nested-Loop Join,并且扫描行数也大大降低了。

所以当遇到 BNL 的 join 语句,如果不方便在关联字段上添加索引,不妨尝试创建临时表,然后在临时表中的关联字段上添加索引,然后通过临时表来做关联查询。

你可能感兴趣的:(MySQL优化(上)2020-08-17)