排序 order by
索引是天然有序的,如果对索引排序,mysql只需要选择升序查询或降序查询即可,不需要专门的排序,效率非常高。
如果排序字段不是索引,mysql就需要将数据查出来再进行排序。
sort_buffer_size
在内存中排序,就肯定会消耗内存空间,mysql的sort_buffer_size就是设置这个空间的大小,sort_buffer_size并非全局共享,是每个线程独占,如果sort_buffer过大,而并发线程又多,就会内存不足。
当sort_buffer满了之后,就会写入临时文件。
调用方法
filesort()
估算排序总行数
estimate_rows_upper_bound()
总大小 = 总叶子节点数 * 页大小
行数 = 2 * (总大小 / 行大小)
总叶子节点数是stat值非实时数据
行大小是字段元数据设置的大小,非实际大小。
所以varcahr设置的值大小, 直接影响到排序的策略。
判断优先级队列是否可用
check_if_pq_applicable()
如果优先级队列可用,查出一条,就放到优先级队列里面排序,然后再查,不用全部查出来,然后排序,效率比较高。
如果不可用,那就用传统的排序方式,通常都是走的这个方式。
查出数据,存放到sort_buffer,直到查询结束或sort_buffer满了之后,save_index()对数据进行排序。
find_all_keys
参数:max_sort_length 默认1024
这个参数是限制每个记录最大排序值,超过这个记录,mysql就只取id和排序字段放到sort_buffer里面,排好序后,再回表查询。
没超过时,就整行放进去排序,排好序就返回,不用回表了。
varchar会按最大计算,并不是按实际大小计算
因为varchar的实际长度是存放在行记录中的,mysql不可能提前查询出行数据,然后在计算实际长度;
而是通过字段的元数据,直接计算大小。
group by
内存分组 ha_heap
通过HashMap数据结构分组
max_heap_table_size 内存表最大值 默认16M
tmp_table_size 临时表大小 默认 16M
取两个变量的最小值为内存临时的大小。
table->file ha_heap
ha_write_row()
写入行,写入到了ha_heap中,并非innodb
write_row ha_heap::write_row()
ha_heap 是一个内存临时表,一个hashMap结构
临时文件分组, innodb临时表
创建一个临时表,分组字段作为主键,主键具有唯一性,当插入重复主键时,就会发送数据合并。
最终临时表的数据就是一份不重复的数据。
mysql会优先使用内存表分组
如果数据量太大,超出最大值,就会在磁盘的临时文件进行分组。
这时候就会利用innodb进行临时表管理。
将内存中已插入的转移到磁盘,后续的直接插入到磁盘临时表。
create_innodb_tmp_table()
create_table()
排序
group by 默认会进行排序
分组后的数据,会经过排序逻辑
统计
在做分组插入临时表时,
会先查询一下临时表是否已有数据,
如果有,就取出来,和要插入的数据做个计算,然后更新(update_row)已有的数据。
如果没有,直接插入
相当于不断在做reduce操作
效率
内存临时表的效率要远高于磁盘临时表
如果某个sql group by 的时间很长, 就要考虑参与计算的数据是否超过了默认的16M.
如果超过了,要么优化sql,减少数据量, 要么增加内存表的大小。
join
join过程
join_buffer
默认256kb,每个线程独占
join过程会面临先查哪个表,再查哪个表,这个是mysql优化器决定的,没有固定。
可以通过执行计划explain看到先查询的哪个表。
通常mysql会选择先查询记录较少的表,这样可以先把它放到join buffer里面,太大可能就放不进去。
nested-loop
通过两层嵌套循环实现
不会使用到join_buffer,通过执行计划explain,如果没有using join buffer 那么就肯定是用的这个。
过程:
外层循环称之为驱动表,内层循环为被驱动表
当驱动表查询出一条后,被驱动表针对外层结果搜索一次。
被驱动表进行查询的条件也就是on的部分,如果所关联的内层表字段不是索引,那么每一次驱动表查询,就会进行一次被驱动表的全表扫描。若驱动表查询数量为n,那么被驱动表被全表扫描的次数为n,所以关联查询一定要建立索引。
如果两表没有on绑定,那么会产生卡迪尔乘积 n*m条结果
如果有外键关联,那么结果数量取决于驱动表。
block nested-loop
针对nested-loop的优化操作, 执行计划有显示using join buffer
上面nested-loop是逐个循环遍历的(for(i=0;i<n;i++),在无索引的情况下,有n次循环就有n次全表扫描,代价太大。
block nested-loop就进行了批量遍历(for(i=0;i<n;i+=m),驱动表每次循环,查询出m条数据,载入到内存(join buffer),然后进行一次被驱动表全表扫描,再进行数据关联。 那么被驱动表的总全表扫描次数为n/m次(m取决于BNL的内存设定大小,内存越大m就越大,次数也越少)。
子查询
简单子查询会被优化成join查询
select * from t1 where id in (select sid from t2 where type=1)
select * from t1 left join t2 on t2.sid=t1.id where t2.type=1
from 部分的子查询,需要建立临时表
内存临时表
磁盘临时表
limit
对innodb来说,还是全表扫描,但是在mysql层面会有一个计数器,用来计算limit,只返回满足次序的记录。
sql_class.cc :: send_data()
send_records, select_limit_cnt
mysql这么处理,会造成无论是取第几页,前面的记录都会被遍历,这就产生了深分页问题。
当页数非常大的时候,mysql就会很慢,mysql层面是无法处理的,只能在其他方面解决。
1 业务上避免深分页,只提供前100页数据,隐藏跳页操作,如电商商品的浏览
2 如果有递增的id或其他索引,可以利用递增索引,提前计算好目标页的索引,然后通过in查询
3 用一个较小的表记录次序,再使用2的方法。
count
count() = count(1) ≈ count(id) < count(字段)
count( ) count(1) 默认是会利用主键索引扫描所有记录,计算总数。
但如果该表有辅助索引,mysql将会扫描辅助索引计算总数。
由于辅助索引的叶子节点只有id,而且辅助索引的叶子数量必定和聚集索引的一样,所以扫描辅助索引的速度要更快。
count(id) 会遍历全表,取出id, 然后判断id是否为空,再count++
由于id是聚集索引,比较快
count(字段) 也是遍历全表,但字段是非索引,需要额外取出记录,消耗更多。