一、msyql 一个查询语句执行顺序是这样的:
bash
from->where->group by/having->select ->distinct -> order by -> limit/offset
二、解析关键字实现以及坑点;
2.1 from 决定使用那张表,哪个索引
2.1.1 from的坑点: 大表驱动小表,会出问题:
因为 a join b 的数据匹配是, 遍历 外表(a表)索引,用外表的索引值去命中内表(b表)的索引,
所以 时间复杂度是 o(aN * log2bN),所以只有小表在前面性能才会好;
2.1.2 from的坑点: join 列无索引:
由上可知,如果都无索引,Nested Loop 直接炸(时间复杂度o(aN*bN),且b还要回表找主键)
2.2 where 行过滤
2.2.1 坑点:索引不满足最左前缀,索引失效
比如联合索引 (a,b,c) where b=n and a=m and c=k; 完全用不上,必须严格 abc 或者你另加索引
2.2.2 坑点:对索引做函数或者计算,索引失效:
sql
where date(create_at)="2025-01-01"
where num + 1= 10
where num = '123' -- 隐式转换
2.2.3 坑点:范围查找破坏联合索引
sql
where a =1 and b > 2 and c=3 -- c=3将无法使用索引命中
log2N快速命中索引的前提是有序, a =1 and b > 2 后的c是无序的或者说是多段有序(比如5,6,8,|2,3,6,|4,7),这样只能遍历所有找到 c=3
2.2.4 坑点:or 破坏索引(b无索引,a的值影响不了b=2,所以只能全表扫描)
where a=1 or b=2
2.2.4 坑点 in /not in
in 是可以用索引的,但是列表过大的话,优化器会认为走索引>全表扫描
not in :必然失效,因为 取反不能利用 命中索引的特性
2.2.5 坑点 not exists 和 not in 对比
not exists 可以用到索引 ,是因为他是一个探测短路机制,外表探测内表存在就可立刻短路返回false,否则探测完不存在既可以记录结果;(这里外表是遍历全索引,但是内表可以快速用索引命中 结果,前文from有说的)
下面的语句就充分利用了索引
sql
SELECT a.*
FROM account a
WHERE NOT EXISTS (
SELECT 1
FROM friend B
WHERE B.PlayerId = A.Id
);
not in 为什么没有有效利用索引: 是因为他需要把所有结果返回,成为一个列表,在对这个列表进行反向处理; 逻辑解析: 首先,外表的遍历行数是一样的;
但是内表遍历行数不一样,这里需要遍历所有结果,但其实是浪费的;比如我需要确认 B.playerId = 5 是否存在,这里查询了所有 B.playerId = 5 的结果(这里其实可以用索引 ),不像 not exists 可以短路。除此之外, 这个结果列表还需要一一比对。这也是花销,这个是用不了索引的。
sql
explain SELECT a.*
FROM mango.account as a
WHERE A.Id NOT IN (
SELECT B.PlayerId
FROM friend as B
);
扩展: 如果如果是对常量列表检索,其实差别不大:
bash
SELECT *
FROM A
WHERE id NOT IN (1, 3, 5, 7);
NOT EXISTS必须有子查询的,其实怎么做还不如直接用not in,有些优化器也是直接优化成 not in
sql
SELECT *
FROM A
WHERE NOT EXISTS (
SELECT 1
WHERE A.id IN (1, 3, 5, 7)
);
【后面再仔细研究 in, not in,exists, join的内容,这里先不讨论】
2.2.5 坑点 like 以 %开头无法确认前缀失效
%可以放结尾, 可以用索引前缀快速确认
sql
where like "xxx%" -- 有效
where like "%xxx" -- 无效
如果必须用前缀模式查找,可以考虑用全文索引 FULLTEXT
2.2.5 is null/ is not null
普通索引: is null 通常生效(索引会存储null值), is not null 生效不了
复合索引: a = 1 and b is null and c= 3
2.3 group by 分组: 多行汇聚一行
分组逻辑:
本质:
1.索引分组(天生有序,顺序分组,根本不用排序)
2.临时表+hash/sort(额外分组就会产生临时表)
分组方式:排序分组 o(nlogn), 哈希分组 o(n);
排序分组 o(nlogn):
先排序,然后把 字段相同的内容放一次实现分组;
哈希分组 o(n)
如果没有索引且不能分组或者分组性价比不高,就会采用hash
哈希其实更快,但是会消耗大量内存(服务器会判断是否够内存处理,不够会特殊分段处理)
2.4 having 对聚合结果过滤
这个是不能用索引的
2.4.1 坑:能写where但写在了 having 将会巨慢,因为不能用索引
2.5 select 取列
确定:
是否回表
是否覆盖索引
2.5.1 select 坑:select *
查询的内容需要回表+io放大
原因是 select *强行需要读取整行数据,第一是会读取一些不必要的数据,第二就是直接放弃覆盖索引的可能,必定会回表
可以稍微了解索引逻辑
【二级索引和主键索引】 :查找一个数据,如果二级索引命中,从二级索引中获得主键索引;或者直接索引主键索引;再或者都没有索引,那就遍历整个主键索引;然后再那具体的值;
**【索引存储和数据结构】**二级索引和主键索引一样都是内存和磁盘都有;主键索引的B+Tree 的叶子节点=整行数据;但是主键索引根节点必定存在内存中;
**【回表查询的消耗】**二级索引查完后,还要到主键索引在查一次;这就有可能会都一次io;如果只是返回一条数据,微乎其微,如果要返回或者匹配十万个数据,那差距将非常的大!
**【磁盘io】**单次查询磁盘io最多两三次;因为B+tree不是二叉树,他的所有节点都是16KB大小;一般非叶子节点能存上千个Key;这样100万行数据最多他也就3层;最多也是两次磁盘IO(主键根节点常驻内存)
【一个节点内如何访问】;节点内的数据是有序的,一般用二分查找+顺序遍历;特别是id > n and id < m 的情况,都是 到指定节点,然后二分到n,在顺序遍历到m.
2.6 DISTINCT 去重
等价于
group by 所有 select 列
sql
select playerId, toid from log_gift_send GROUP BY PlayerId, toid
--等价于
select DISTINCT playerId, toid from log_gift_send
所以 distinct的坑点 和group by 是一样的
2.7 order by -- 排序(最大性能杀手,阻塞因子,不读完不能吐)
本质:结果集排序
排序方式:
- 顺着索引读(零成本)
2. filesort(全局排序)
为什么需要做全局排序,而不是做结果集排序 ;
因为你不能确定结果集的长度;其实如果后面加了 limit n;且n的值是比较小的,那么就可以用一个长度=n的堆做排序;常用topN的大小根堆算法 ;
不能确定长度,如果长度是m;那复杂读将是 o(m*m);那mysql服务器炸了; 所以在没有索引的情况下,选择最稳定的 o(mlogm)全局排序是最好的,
2.7.1 坑点: group by a,c 和 order by a,c 是否可以用一个排序结果;
如果他俩都是sort 排序的话,可以复用,
如果 group by 没有排序,而是用hash实现分组,那么你需要重新生成sort;
2.7.2 坑点: group by a,c order by c,a 产生filesort
还有就是 group by a,c order by c,a 是行不通;哪怕都有索引,但是结果集是按 a,c 排序的, 你的c,a索引对于这个结果集一点用没有;
2.8 limit/offset
offset n 放弃结果集 的前n条;性能爆炸语句,不能用
limit m; 取m行结果: id > 0 limit n搭配非常合适;
2.8.1 坑:
有个问题 limit 只是获取结果集的m行;group by order by 对整个结果集的处理是不能通过limit避免不了的;