(1)索引创建
数据基数是指该字段所有数据去重之后的个数,比如性别就不建议建索引,性别索引对应的树结构过于集中;
可以通过distinct统计字段数据基数;
(2)索引使用
where条件字段为索引字段,尽量使匹配条件为“=”;
四个字段执行的效率差不多,一般直接用count(*)即可;
字段有索引:count(*)≈count(1)>count(字段)>count(主键 id)
//字段有索引,count(字段)统计走二级索引,二 级索引存储数据比主键索引少,所以count(字段)>count(主键 id)
字段无索引:count(*)≈count(1)>count(主键 id)>count(字段)
//字段没有索引count(字段)统计走不了索引, count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)
count优化方案:
1.预估count,不准确
show table status like 'employees';
2.将count维护到redis里,但是不能保证强一致性;
3.将count保存到mysql中,新增修改表数据时同时维护该统计数据,放在一个事务里;
对索引字段做函数、计算操作,可能会破坏索引值的有序性,因此优化器就放弃走搜索树功能;
隐式类型转换、不同字符集字段关联查询、字符串不加单引号不走索引,实际上也做了函数操作;
例如,查询日期时,不要用函数表达式,这样就破坏了索引有序性。解决:通过范围查找,走索引;
例如,where left(name,3) = ‘jay’;
!= 、<>
not in 、not exists
< 、 > 、<= 、>=
mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引;
使用后会导致索引失效,全表扫描;
不一定走索引;
在数据量大的情况下走索引,数据量小的情况下全表扫描;
is null,is not null 一般情况下也无法使用索引;
一张表一般建议1到3个联合索引,覆盖80%的使用场景,其他场景通过特定索引来解决;
如果已经有了(a,b)这个联合索引,那么就不需要a的单独索引了;
使得查询列都在索引列上,避免回表;
SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
有时不走索引,因为mysql引擎会根据检索比例、表大小等因素判断是否需要走索引;
mysql内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不如就全表扫描;
可以使sql语句强制走索引,比较下不走索引与走索引查询效率;
给范围查询的字段建立单个索引;
将大范围变成小范围,避免mysql优化器判断查询为大数据量从而走全表扫描,索引失效;
没有添加 order by,默认通过主键排序(asc);
示例表:
CREATE TABLE `employees` (
3 `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
4 `name` VARCHAR ( 24 ) NOT NULL DEFAULT '' COMMENT '姓名',
5 `age` INT ( 11 ) NOT NULL DEFAULT '0' COMMENT '年龄',
6 `position` VARCHAR ( 20 ) NOT NULL DEFAULT '' COMMENT '职位',
7 `hire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
8 PRIMARY KEY ( `id` ),
9 KEY `idx_name_age_position` ( `name`, `age`, `position` ) USING BTREE 10
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT = '员工记录表';
分页查询语句:
select * from employees limit 90000,5;
表示从表 employees 中取出从 10001 行开始的 10 行记录。看似只查询了 10 条记录,实际这条 SQL 是先读取 10010 条记录,然后抛弃前 10000 条记录,然后读到后面 10 条想要的数据。因此要查询一张大表比较靠后的数据,执行效率 是非常低的。
优化:
select * from employees where id > 90000 limit 5;
这个sql语句虽然可以使用id索引,但是必须保证id是连续的。实际应用场景中,总会有删除操作,导致id不连续,此种优化方案需要满足两个条件:1.主键自增且连续;2.结果是按照主键排序的(asc);
非主键索引的分页语句:
select * from employees ORDER BY name limit 90000,5;
这个sql语句也是没有用到索引,因为mysql会根据数据量大小来判断是否走索引,非覆盖索引;
优化:
select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
衍生结果是通过覆盖索引查询得到id,走了覆盖索引,效率高,然后通过id结果与原表链表,效率是很高的;
原sql走了是filesort排序,优化后的排序走的是索引排序;
KK%一般都会走索引;
例如,like ‘%jay’,查询字段是覆盖索引字段可以解决这种like的全表扫描;
优化:
使用覆盖索引,查询字段必须是建立了覆盖索引的字段,仅仅是少了回表步骤?;
借助搜索引擎,比如ES;
什么是索引下推了?
对于辅助的联合索引(name,age,position),正常情况按照最左前缀原则,SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager'
这种情况只会走name字段索引,因为根据name字段过滤完,得到的索引行里的age和position是无序的,无法很好的利用索引。
在MySQL5.6之前的版本,这个查询只能在联合索引里匹配到名字是 ‘LiLei’ 开头的索引,然后拿这些索引对应的主键逐个回表,到主键索引上找出相应的记录,再比对age和position这两个字段的值是否符合。
MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。使用了索引下推优化后,上面那个查询在联合索引里匹配到名字是 ‘LiLei’ 开头的索引之后,同时还会在索引里过滤age和position这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。
索引下推会减少回表次数,对于innodb引擎的表索引下推只能用于二级索引,innodb的主键索引(聚簇索引)树叶子节点上保存的是全行数据,所以这个时候索引下推并不会起到减少查询全行数据的效果。
通过explain的Extra列可以看出排序性能,比如Using fileSort
就表示使用了文件系统排序,而没有使用性能更高的sort_buffer内存排序;
排序字段的asc和desc须与索引字段的asc保持一致,才能使用到索引排序,Mysql8以上版本有降序索引可以支持该种查询方式;
排序总结:
优化总结:
1、MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index效率高,filesort效率低。
2、order by满足两种情况会使用Using index。
- order by语句使用索引最左前列。
- 使用where子句与order by子句条件列组合满足索引最左前列。
3、尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀法则。
4、如果order by的条件不在索引列上,就会产生Using filesort。
5、能用覆盖索引尽量用覆盖索引
6、group by与order by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则。对于group by的优化如果不需要排序的可以加上order by null禁止排序。注意,where高于having,能写在where中的限定条件就不要去having限定了。
文件排序分为单路排序和双路排序:
单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可以看到sort_mode信息里显示< sort_key, additional_fields >或者< sort_key, packed_additional_fields >
双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段;用trace工具可以看到sort_mode信息里显示< sort_key, rowid >;
MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来判断使用哪种排序模式。
如果字段的总长度小于max_length_for_sort_data,那么使用单路排序模式;
如果字段的总长度大于max_length_for_sort_data,那么使用双路排序模式。
开启trace工具会影响mysql性能,所以只能临时分析sql使用,用完之后立即关闭;
mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; --开启trace
mysql> select * from employees where name > 'a' order by position;
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;
mysql> set session optimizer_trace="enabled=off"; --关闭trace
通过trace工具展示的mysql选择索引的过程,比如:索引使用范围、使用该索引获取的记录是否按照主键排序、 是否使用覆盖索引、索引扫描行数、索引使用成本、是否选择该索引等指标判断是否使用索引,部分分析信息如下:
关联字段加索引;
小表驱动大表,用straight_join指明那个是驱动表,适用于inner join;
小表的定义:在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
示例表:
CREATE TABLE `t1` (
`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;
-- t2表跟t1表一样,t1表10w条数据,条表100条数据;
mysql的表关联常见有两种算法:
Nested-Loop Join 算法
Block Nested-Loop Join 算法
1、 嵌套循环连接 Nested-Loop Join(NLJ) 算法
一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。
select * from t1 inner join t2 on t1.a= t2.a;
优化器一般会优先选择小表做驱动表。所以使用 inner join 时,排在前面的表并不一定就是驱动表。
当使用left join时,左表是驱动表,右表是被驱动表,当使用right join时,右表时驱动表,左表是被驱动表, 当使用join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表。
使用了NLJ算法。一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer
则表示使用的 join 算法是 NLJ。
上面sql的大致流程如下:
- 从表 t2 中读取一行数据(如果t2表有查询过滤条件的,会从过滤结果里取出一行数据);
- 从第 1 步的数据中,取出关联字段 a,到表 t1 中查找;
- 取出表 t1 中满足条件的行,跟 t2 中获取到的结果合并,作为结果返回给客户端;
- 重复上面 3 步。
整个过程会读取 t2 表的所有数据(扫描100行),然后遍历这每行数据中字段 a 的值,根据 t2 表中 a 的值索引扫描 t1 表 中的对应行(扫描100次 t1 表的索引,1次扫描可以认为最终只扫描 t1 表一行完整数据,也就是总共 t1 表也扫描了100 行)。因此整个过程扫描了 200 行。 如果被驱动表的关联字段没索引,使用NLJ算法性能会比较低(下面有详细解释),mysql会选择Block Nested-Loop Join
算法。
2、 基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法
把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。
select * from t1 inner join t2 on t1.b= t2.b;
其中,b字段没有索引;
上面sql的大致流程如下:
- 把 t2 的所有数据放入到 join_buffer 中
- 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比
- 返回满足 join 条件的数据
整个过程对表 t1 和 t2 都做了一次全表扫描,因此扫描的总行数为10000(表 t1 的数据总量) + 100(表 t2 的数据总量) = 10100。并且 join_buffer 里的数据是无序的,因此对表 t1 中的每一行,都要做 100 次判断,所以内存中的判断次数是 100 * 10000= 100 万次。
这个例子里表 t2 才 100 行,要是表 t2 是一个大表,join_buffer 放不下怎么办呢?
join_buffer 的大小是由参数 join_buffer_size
设定的,默认值是 256k。如果放不下表 t2 的所有数据话,策略很简单,就是分段放。
比如 t2 表有1000行记录, join_buffer 一次只能放800行数据,那么执行过程就是先往 join_buffer 里放800行记录,然 后从 t1 表里取数据跟 join_buffer 中数据对比得到部分结果,然后清空 join_buffer ,再放入 t2 表剩余200行记录,再 次从 t1 表里取数据跟 join_buffer 中数据对比。所以就多扫了一次 t1 表。
(3)优化原则
不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗?
这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立索引。
比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则。
索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段,其值不是男就是女,那么该字段的基数就是2。
如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。
一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查找的优势来。
尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会比较小,此时你在搜索的时候性能也会比较好一点。
当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间也是有必要的。
对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY index(name(20),age,position)。
此时你在where条件里搜索的时候,如果是根据name字段来搜索,那么此时就会先到索引树里根据name字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来完整的name字段值进行比对。
但是假如你要是order by name,那么此时你的name因为在索引树里仅仅包含了前20个字符,所以这个排序是没法用上索引的, group by也是同理。所以这里大家要对前缀索引有一个了解。
在where和order by出现索引设计冲突时,到底是针对where去设计索引,还是针对order by设计索引?到底是让where去用上索引,还是让order by用上索引?
一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。
因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。
可以根据监控后台的一些慢sql,针对这些慢sql查询做特定的索引优化。