索引
-
覆盖索引 优先从辅助索引里找,数据量小(叶子只有id),如
select id/count(*) from table where other_index = xxx; -
不使用索引的情况, 除了一般说的is null, !=, like %xxx,外
使用辅助索引查时,若查询结果为id外的且结果占整张表过大(一般20%)的会全表扫描
因为使用辅助索引查到id后,要将这些id去聚集索引里找,这时为随机读,磁盘随机读一般较慢(固态会高一些),mysql优化器会选择全表扫(顺序读) 对索引进行函数操作、数据类型转换(如字符串转数字)、数据编码转换(utf8转utf8mb4) -
Index Hint(索引提示) use/force/ignore index等
-
Multi-Range Read (MRR)
为了减少随机访问 (Explain中Extra字段显示 Using MRR),适用于range、ref、eq_ref类型的查询
1 使得数据访问变得较为顺序,查询辅助索引时,先依据辅助索引查找的的主键重新排序,然后用排序好的的主键进行书签查找 -
Index Collection Pushdown
在存储引擎层就对数据进行过滤,减少sql层对数据的读取 (Explain中Extra字段显示 Using Index Condition)
建立索引注意事项
主索引建议自增(顺序存储,否则需要移动后面的数据),辅助索引不建议同方向(高并发更新情况引起最后页的竞争)
sql优化
什么情况下设置了索引但无法使用,索引无效
- 以”%”开头的LIKE语句,模糊匹配:红色标识位置的百分号会导致相关列的索引无法使用
- Or语句前后没有同时使用索引
- 数据类型出现隐式转化(如varchar不加单引号的话可能会自动转换为int型,会使索引无效,产生全表扫描。)
- 在索引列上使用IS NULL 或IS NOT NULL操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的办法处理,例如:数字类型,判断大于0,字符串类型设置一个默认值,判断是否等于默认值即可
- 在索引字段上使用not,<>,!=,eg<> 操作符(不等于):不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 用其它相同功能的操作运算代替,如 a<>0 改为 a>0 or a<0
- 对索引字段进行计算操作
- 在索引字段上使用函数 sql优化
不走索引的情况
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
条件字段函数操作
隐式类型转换
隐式字符编码转换
MySQL实战45讲/19
基本操作
-
查看表的存储引擎
1 查看table_nameshow create table table_name;2 查看MyDB的所有表引擎select table_catalog ,table_schema ,table_name ,engine from information_schema.tables where table_schema='MyDB'; //and engine='MyISAM';
优化
innodb内存管理
在redo log工作时,更新的数据若在内存,则会直接更新内存,写入redo log、bin log提交事物;下次读取是直接读取内存数据。
这个内存就是innodb查询结果后在内存中的缓存,由innodb_buffer_pool_size控制。
如何淘汰旧数据:改进的最近最少使用算法 (Least Recently Used, LRU)
普通LRU算法:队列头是最新的数据,刚查询出来或者再次访问的数据,队尾为即将淘汰的数据
问题:一个大量读取的操作会将所有之前的淘汰,而新数据又不常用也马上淘汰,那么这段时间缓存命中率很低
改进后:分段5:3的young:old,刚查询出的数据在old的队列头(6/8位置),
再次访问时
young的数据到队列头,old的数据若存在时间小于innodb_old_blocks_time(默认1秒)则不变,超过才会到队列头
join 是如何工作的
select * from t1 join t2 on (t1.a=t2.a);
-
被驱动表有索引 Index Nested-Loop Join
1.优化器选择其中一个为驱动表(t1),一般原则:驱动表记录少,被驱动表有索引等
2.读入t1一条数据R,到t2中查询,有索引走索引
3.取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
4.重复执行步骤2、3,直到表t1的末尾循环结束。
为什么选表记录少的做驱动表?
若t1:N行,t2:M 行,
那么扫描行数:N + N*logM,判断次数 N *logMtype:ref

MRR优化:上面第二步会变成:将数据读入join buffer,排序后顺序读,
扫描t2一次,N次比较,扫描行数:N + logM,判断次数还是 N*logMExtra:Using MRR

-
被驱动表无索引 Block Nested-Loop Join
上面第二步会变成:将数据读入join buffer,排序后顺序读,
扫描行数:N + M,判断次数 N*M若join buffer不够,则分多次读取,多次扫描t2,由join_buffer_size参数控制,
扫描行数:N + n *M(n为size(t1)/join_buffer_size),判断次数 N *MExtra:Using join_buffer(Block Nested Loop)

-
写法区别
select * from a straight_join b on(a.f1=b.f1); /*Q1*/ select * from a join b on(a.f1=b.f1); /*Q2*/ select * from a left join b on(a.f1=b.f1); /*Q3*/ select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2);/*Q4*/ select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q5*/Q1 Q2 straight_join为指定驱动表,这里a一定为驱动表,而join可能会被优化为b
Q2 Q3 left join左连接,a的数据一定会出现,匹配不到的b表的列名为null,同样会被优化
Q4 Q5 Q4匹配不到的b表的列名会出现null,Q5不会
下面四句其实一样:
select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); select * from a join b on(a.f1=b.f1) where (a.f2=b.f2); select * from a join b where (a.f1=b.f1) and (a.f2=b.f2); select * from a ,b where (a.f1=b.f1) and (a.f2=b.f2); -
优化
- 在被驱动表上建索引,驱动表数据量少
- 若在被驱动表上建索引很浪费资源,使用临时表或者查询出来在客户端hash join
讲义例子: t1数据量一般,t2很大,但是只要2000行(感觉优化器会字段选择t2当驱动表) select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000; 使用: create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb; insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);
order by是如何工作的
select city,name,age from t where city='杭州' order by name limit 1000;
- 无覆盖索引,有city索引
- 初始化sort_buffer,确定放入name、city、age这三个字段;
- 从索引city找到第一个满足
city='杭州'条件的主键id; - 到主键id索引取出整行,取name、city、age三个字段的值,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复步骤3、4直到city的值不满足查询条件为止,对应的主键id也就是图中的ID_Y;
- 对sort_buffer中的数据按照字段name做快速排序;
- 按照排序结果取前1000行返回给客户端。
相关参数 sort_buffer_size 排序内存最大值 max_length_for_sort_data 排序每行最大值
若行数过多,超过了sort_buffer_size,会利用磁盘临时文件辅助排序,先排好一部分放入磁盘归并排序
若列数过长,超过了max_length_for_sort_data,第一步只取出name id字段,排序好后第七步再检索一遍聚集索引
- 有覆盖索引
有复合索引(city,name) 或者(city,name,age)
(city, name):
- 从索引(city,name)找到第一个满足
city='杭州'条件的主键id; - 到主键id索引取出整行,取name、city、age三个字段的值,作为结果集的一部分直接返回;
- 从索引(city,name)取下一个记录主键id;
- 重复步骤2、3,直到查到第1000条记录,或者是不满足
city='杭州'条件时循环结束。
(city,name,age):
- 从索引(city,name,age)找到第一个满足
city='杭州'条件的记录,取出其中的city、name和age这三个字段的值,作为结果集的一部分直接返回; - 从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
-
重复执行步骤2,直到查到第1000条记录,或者是不满足
city='杭州'条件时循环结束。
group by是如何工作的
create table t1(id int primary key, a int, b int, index(a));
select id%10 as m, count(*) as c from t1 group by m;
explain一下:
Using index,表示这个语句使用了覆盖索引,选择了索引a,不需要回表;
Using temporary,表示使用了临时表;
Using filesort,表示需要排序。
这个语句的执行流程是这样的:
- 创建内存临时表,表里有两个字段m和c,主键是m;
- 扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;
如果临时表中没有主键为x的行,就插入一个记录(x,1);
如果表中有主键为x的行,就将x这一行的c值加1; - 遍历完成后,再根据字段m做排序,得到结果集返回给客户端。
**优化
***添加索引
alter table t1 add column z int generated always as(id % 100), add index(z);
explain后只有using index

***不适合添加索引的情况
若数据量很大,添加索引太耗资源,而且内存临时表肯定放不下,则可用SQL_BIG_RESULT,直接使用磁盘,避免了先使用内存表发现内存不够,再用磁盘
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
explain后只有Using index,Using filesort
*** 优化小结
-
如果对group by语句的结果没有排序要求,要在语句后面加 order by null;
-
尽量让group by过程用上表的索引,确认方法是explain结果里没有Using temporary 和 Using filesort;
-
如果group by需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大tmp_table_size参数,来避免用到磁盘临时表;
-
如果数据量实在太大,使用SQL_BIG_RESULT这个提示,来告诉优化器直接使用排序算法得到group by的结果。
其他
前缀索引,前缀区分度高-email
使用前几位即可,减小存储
给表email字段前六位添加索引`alter table SUser add index index2(email(6));`
分析区分度 可以用5%标准
```
select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
```
后缀的区分度高-身份证
1反转存储
`select field_list from t where id_card = reverse('input_id_card_string');`
2hash存储
`alter table t add id_card_crc int unsigned, add index(id_card_crc);`
`select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'`
首先,它们的相同点是,都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在[ID_X, ID_Y]的所有市民了。同样地,hash字段的方式也只能支持等值查询。
它们的区别,主要体现在以下三个方面:
从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而hash字段方法需要增加一个字段。当然,倒序存储方式使用4个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个hash字段也差不多抵消了。
在CPU消耗方面,倒序方式每次写和读的时候,都需要额外调用一次reverse函数,而hash字段的方式需要额外调用一次crc32()函数。如果只从这两个函数的计算复杂度来看的话,reverse函数额外消耗的CPU资源会更小些。
从查询效率上看,使用hash字段方式的查询性能相对更稳定一些。因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
[参考MySQL实战45讲/11讲]()
查询结果只有一条仍然慢的几种情况
等MDL锁
等flush(flush tables操作,关闭对应的表,确保数据刷新至磁盘)
等行锁
回滚至一致性读的概念,一致性读时若该数据被其他线程操作了很多次,需要回滚至当前事务开始时期
自增主键不连续
唯一键冲突是导致自增主键id不连续 事务回滚