mysql索引优化系列(二)

一、limit优化

之前的member会员表,联合索引为KEY `idx_name_age_address` (`name`,`age`,`address`),表里插入了十万条数据,一般情况下分页查询的sql语句:

select * from member limit 90000,10;
explain select * from member limit 90000,10;

mysql索引优化系列(二)_第1张图片

执行计划是全表扫描,底层执行过程:首先,如果没有排序字段,默认按照id进行排序,查询前90010条件数据出来,然后去掉前面90000条件数据,最后剩下的10条就是查询结果,其实这种随着数据量越来越大,越往后面翻页,一次查询的数据只会多不会少,这样查询就会存在问题

1、根据自增主键排序

select * from member where id > 90000 limit 10;
explain select * from member where id > 90000 limit 10;

mysql索引优化系列(二)_第2张图片

执行计划显示出使用了主键id索引的范围查询,这种情况,如果中间id有断层,查询的数据就会有问题;比如id为1的被删除了,理论上就会是id为90002的开始到90011结束,但是如果使用id筛选过滤的话就会有问题,具体体现如下(把id为1的数据删了):

select * from member limit 90000,10;
select * from member where id > 90000 limit 10;

mysql索引优化系列(二)_第3张图片

mysql索引优化系列(二)_第4张图片

2、根据普通索引字段排序分页查询

这个时候就需要对sql再做一些优化,根据二级索引b+树的结构,先根据字段排好序(一般情况下不会根据id排序),分页查出前10条数据的id,这样就可以避免回表而使用到了覆盖索引,接着再根据这些id关联查询出来的就是结果集

select * from member t1 inner join (select id from member order by name limit 90000,10) t2 on t1.id = t2.id;
explain select * from member t1 inner join (select id from member order by name limit 90000,10) t2 on t1.id = t2.id;

根据最左前缀的字段name进行排序分页查询的结果如下:

mysql索引优化系列(二)_第5张图片

查询计划首先是id为2的先执行,类型为衍生查询,使用了覆盖索引,三个索引字段都用到了,并且使用了索引排序;第二执行是table为执行,虽然是全表扫描,但是只有10个id结果集而已;第三执行的是table为t1的执行,查询类型为eq_ref,使用了主键关联,所以整个过程是相当快的

二、join优化

#创建表
CREATE TABLE `table1` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `a` int(11) DEFAULT NULL,
 `b` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#创建一个和table1一样的表
create table table2 like table1;

drop procedure if exists insert_table1;
CREATE PROCEDURE insert_table1 () BEGIN
	DECLARE
		i INT;
	
	SET i = 1;
	WHILE
			( i <= 100000 ) DO
			INSERT INTO table1 ( a, b )
		VALUES
			( i, i );
		
		SET i = i + 1;
		
	END WHILE;
END;


drop procedure if exists insert_table2;
CREATE PROCEDURE insert_table2 () BEGIN
	DECLARE
		i INT;
	
	SET i = 1;
	WHILE
			( i <= 100 ) DO
			INSERT INTO table2 ( a, b )
		VALUES
			( i, i );
		
		SET i = i + 1;
		
	END WHILE;
END;

call insert_table1();
call insert_table2();

 有两个表table1和table2,table1有100000条数据,table2有100条数据,索引字段也都是a,b就是普通字段

1、嵌套循环连接Nested-Loop Join(NLJ)算法

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

执行过程:根据执行计划,首先执行的是table列为t2的,进行的是全表扫描;然后执行的是table为t1,是索引进行关联,table2是驱动表,table1是被驱动表;t2是索引关联查询,只是在索引树上查找到主键,然后回表一次,table2全表扫描一行数据,table1跟着全表扫描一行数据,执行过程我画了张草图

mysql索引优化系列(二)_第6张图片

2、基于块的嵌套循环Block Nested-Loop Join(BNL)算法

EXPLAIN select * from table1 t1 inner join table2 t2 on t1.b = t2.b;

 mysql8.0之前执行的结果图

mysql8.0执行结果图

执行过程:table列值为t2的先执行,t2作为驱动表用的全表扫描,扫描了100行放到join_buffer中(放得下的情况,如放不下就分批),接着table1全表扫描100000行去join_buffer中进行数据匹配,匹配成功返回,下面的图是对这段话的说明:

mysql索引优化系列(二)_第7张图片

show variables like '%join_buffer_size%';#查看join_buffer大小

 对于驱动表的定义:join连接数据量较少的表,也可以使用straight_join指定驱动表,但不适用于left join和right join,因为它们已经指定了驱动表,straight_join用法:

select * from table1 t1 straight_join table2 t2 on t1.b = t2.b;

假设字段无索引还使用了NLJ算法,总共就会扫描100000*100行,table2扫描一行数据到table1表遍历查询,因为没有走索引,这个过程是非常慢的,table2表每扫描一行就要去table1去遍历扫描,单次最坏的情况要十万次才找到结果集,所以就是100000*100次行扫描

一般情况下如果对性能有比较高的要求的系统,都不推荐使用join关联,如果非要用最好不要超过三个,优化起来比较困难,优化原理,小表驱动大表,join连接字段大表最好建有索引

三、in和exists

1、in的查询优化

小表驱动大表

#table1十万条数据 table2一百条数据
EXPLAIN select * from table1 where id in (select id from table2);#正确示例
EXPLAIN select * from table2 where id in (select id from table1);#错误示例

 table2是驱动表,走的是普通索引,因为id在二级索引树上就可以找到,所以选择普通索引;table1使用的是主键关联查询,直接就可以在主键索引树上获取数据;

 第二种可能是mysql做的优化,table2数据量比较少,虽然位置放的不合理,还是用table2作为驱动表,先进性table2的全表扫描,比之前的慢一点,之后的差不多

2、exists查询优化

如果能用in就不要用exists

explain select * from table2 t2 where exists (select * from table1 t1 where t1.id = t2.id);#正确示例
explain select * from table1 t1 where exists (select * from table2 t2 where t2.id = t1.id);#错误示例

 第一条sql,t2作为驱动表,虽然是全表扫描,但是数据量没有t1那么多,然后再用t1主键关联,这个和in位置写错了差不多

第二条sql,t1全表扫描,t1十万条数据,显然这个是很慢的,然后主键关联肯定时间也长

四、count查询优化

count(*),count(1),count(id),count(字段)

count的不同写法,要说快慢其实差不多,具体要分两种情况,一种是字段有索引,一种是字段无索引

1、有索引

count(*)≈count(1)>count(字段)>count(id)

count(*)的执行过程,底层不取值,按行累加

count(1)和count(字段)差不多,count(1)不需要取出字段值,就按照常量1做统计,count(字段还需要取值),count(1)要比count(字段要快)

count(字段)比count(id)要快,因为字段有索引情况下,二级索引比主键索引树数据少,扫描效率肯定快

2、无索引

count(*)≈count(1)>count(id)>count(字段)

上面已经说明count(*)和count(1),而count(id)和count(字段),没有索引自然count(id)要快,毕竟是在索引树上进行的内存操作

EXPLAIN select count(*) from member;
EXPLAIN select count(1) from member;
EXPLAIN select count(id) from member;
EXPLAIN select count(name) from member;

为什么count(id)也用到了二级索引,因为二级索引树的叶子节点数据量少,可能mysql底层也做了优化

3、优化方法

  • myisam类型表的总行数

myisam存储引擎的表会把总记录数维护在磁盘上

EXPLAIN select count(*) from test_myisam;

 innodb为什么不也设置一个总行数呢,因为myisam不支持事务,innodb需要支持事务,如果维护了,代价高

  • show table status
show table status like 'member';

 对member表的总行的估值

  • 将总行数维护到redis中

在数据删除和新增时,往redis中进行表key值加减,但是无法保证一致性

  • 增加表维护总行数

新增一个表table_count,存表明和这个表的行数,新增和删除时进行表table_count数据的更改,这样就可以事务的一致性了

五、mysql数据类型选择

1、数值类型

类型 大小
TINYINT 1字节
SMALLINT 2字节
INT 4字节
BIGINT 8字节
FLOAT 4字节
DOUBLE 8字节
DECIMAL DECIMAL(M,D) ,如果M>D,为 M+2否则为D+2

 如果整型没有负数,可以指定字段为无符号类型

对精度有要求,字段要设置成DECIMAL

2、时间类型

类型 大小
DATE 3字节
TIME 3字节
YEAR 1字节
DATETIME 8字节
TIMESTAMP 4字节

 如果只是日期,那就选date类型,这样可以减小筛选的粒度

datetime类型最大值是9999-12-31 23:59:59,而timestamp类型最大值是2038-01-19 03:14:07,衡量之后再做选择

3、字符串类型

类型 大小
CHAR 0-255字节
VARCHAR 0-65535 字节
TINYBLOB 0-255字节
TINYTEXT 0-255字节
BLOB 0-65535字节
TEXT 0-65535字节

 如果已经确定了字符长度最好用char类型,比如char(4),而只存了一个a,就会在后面补空格,查询出来,mysql会帮你去空格,这样查询也会耗费性能;

如果是tinyint(4)就会进行0填充,加入存了一个2,结果就是0002,查询会去零,括号里的只是显示宽度

blob字段分开一个表存,只存id和blob内容就行,保证两个表的的双写一直就可以,这样可以减少主键索引的空间,提高查询速度

你可能感兴趣的:(mysql,数据库)