MySQL实战之SQL
文章目录
SQL篇
count(*)这么慢,我该怎么办
count实现方式
- MyISAM总行数存在磁盘上, 执行
count(*)
时会直接返回, 效率高, 但是有where
条件, 一样要扫描, 也没这么快- 不支持事务
- InnoDB需要把数据一行一行地从引擎里面读出来,然后累积计数
- 同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”也是不确定的。所以不能像MyISAM将行数存起来。
InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于count(*)这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL优化器会找到最小的那棵树来遍历。
在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
show table status
命令返回行数 TABLE_ROWS
使用采样统计估算得来的,不准确。官方文档说误差可能达到40%到50%。
如果你现在有一个页面经常要显示交易系统的操作记录总数,到底应该怎么办呢?答案是,我们只能自己计数。
计数方法
-
缓存系统Redis保存总行数,每插入一行Redis计数加1,每删除一行Redis计数减1。
- 缓存系统丢失更新
- Redis计数持久化存储,Redis异常重启后,从存储Redis数据地方读回来,刚刚加1的计数操作也会丢失。
- 总之,将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使Redis正常工作,这个值还是逻辑上不精确的。
-
在数据库保存计数,单独使用计数表
- InnoDB是支持崩溃恢复不丢数据的
- 计数表也支持“事务”,不会出现采用Redis计数时,在事务中计数对不上的问题
- 从并发角度看,先插入操作记录,再更新计数表,减小行锁竞争
计数放在Redis里面,不能够保证计数和MySQL表里的数据精确一致的原因, 是 这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。 而把计数值也放在MySQL中,就解决了一致性视图的问题。
思考,上述两种方式只适合全表总行数总计,不适合加where
条件时使用
比如电商后台管理系统中有很多列表,数据量大,又有筛选条件,可以使用ES
来解决
count(*)、 count(字段)、 count(主键id)、 count(1) 用法和性能
-
count(字段)
- 如果这个“字段”是定义为
not null
的话,一行行地从记录里面读出这个字段,判断不能为null
,按行累加; - 如果这个“字段”定义允许为
null
,那么执行的时候,判断到有可能是null
,还要把值取出来再判断一下,不是null
才累加。
- 如果这个“字段”是定义为
-
count(主键id)
- InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。
- server层拿到id后,判断是不可能为空的,就按行累加。
-
count(1)
- InnoDB引擎遍历整张表,但不取值。
- server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
- 比
count(主键id)
快,不用解析数据行。
-
count(*)
- 并不会把全部字段取出来,而是专门做了优化,不取值。
count(*)
肯定不是null
,按行累加。
结论是:按照效率排序的话,count(字段)
<count(主键id)
<count(1)
≈count(*)
,所以我建议你,尽量使用count(*)
。
“orderby”是怎么工作的
select city,name,age from t where city='杭州' order by name limit 1000;
全字段排序
Extra字段中“Using filesort”表示需要排序
sort_buffer
:MySQL给每个线程分配一块内存用于排序。
索引执行流程:
-
初始化sort_buffer,确定放入name、city、age这三个字段;
-
从索引
city
找到第一个满足city='杭州'
条件的主键id,也就是图中的ID_X; -
到主键id索引取出整行,取
name
、city
、age
三个字段的值,存入sort_buffer
中; -
从索引
city
取下一个记录的主键id; -
重复步骤3、4直到
city
的值不满足查询条件为止(city!='杭州'
),对应的主键id也就是图中的ID_Y; -
对
sort_buffer
中的数据按照字段name做快速排序; -
按照排序结果取前1000行返回给客户端。
用name
排序动作来说明
- 排序数据量小于
sort_buffer_size
,排序在内存中完成 - 排序数据量太大,内存放不下,不得不利用磁盘临时文件辅助排序,外部排序
- 外部排序一般使用归并排序算法
- 查看
OPTIMIZER_TRACE
结果中number_of_tmp_files
是否使用临时文件number_of_tmp_files
表示的是,排序过程中使用的临时文件数number_of_tmp_files=12
表示将需要排序的数据分成12份,每一份单独排序后存在这些临时文件中。然后把这12个有序文件再合并成一个有序的大文件。sort_buffer_size
越小,需要分成的份数就越多,number_of_tmp_files
值就越大sort_mode
中packed_additional_fields
排序过程对字符串做了“紧凑”处理,按实际空间来分配,不是按定义varcha(16)
来分配
rowid排序
如果返回的字段多,那么sort_buffer放的字段数太多,内存能同时放下的行数很少,要分成很多个临时文件,排序的性能回很差。 所以单行很大,全字段排序方法效率不太好。
|
|
设置后,MySQL认为单行太大,要换个算法。
新的算法sort_buffer
中只有要排序的列(name)和主键id
执行流程差异,因sort_buffer
中没有city
和age
,最后按照id的值回到原表中取出name
、city
、age
字段返回给客户端
- 查看
OPTIMIZER_TRACE
结果中number_of_tmp_files
是否使用临时文件sort_mode
值变成了<sort_key, rowid>
,表示参与排序的只有name和id这两个字段。number_of_tmp_files
变成10了,每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。
全字段排序 VS rowid排序
MySQL担心内存太小,才会采用rowid排序算法,一次可以排序很多的行,但是需要回原表去取数据。
MySQL认为内存过大,优先选择全字段排序,直接从内存取数据,不需要回原表。
MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
解决上述问题,避免排序可以考虑使用覆盖索引alter table t add index city_user_age(city, name, age);
如何正确地显示随机消息
内存临时表
“InnoDB”表,对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。
|
|
explain分析执行情况: Extra字段显示Using temporary,表示的是需要使用临时表;Using filesort,表示的是需要执行排序操作。
从OPTIMIZER_TRACE
结果来看,未使用文件排序,5.6
版本后引入了新算法:优先队列排序算法
|
|
小结一下:order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法。
磁盘临时表
tmp_table_size
配置了内存临时表大小,超过就会转成磁盘临时表
磁盘临时表internal_tmp_disk_storage_engine
默认值是InnoDB
随机排序方法
以取3条数据为例 都在应用程序控制,数据库只处理读写
- 计算出总行数 c
- 依据 floor(c * rand()) 算出一个数字,计算3次
- 使用 limit Y,1 语句 执行3次,Y是上一步计算出来的
最终还是从不同的业务不同的分析
到底可不可以使用join
|
|
使用的算法
Index Nested-Loop Join
,可以用上被驱动表的索引- 从表 t1 中读入一行数据 R;
- 从数据行R中,取出a字段到表t2里去查找;
- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
- 重复执行步骤1到3,直到表t1的末尾循环结束。
Block Nested-Loop Join
,扫描行数就会过多,尤其是在大表。可能会扫描被驱动表很多次,会占用大量的系统资源。这种join尽量不要使用。- 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
- 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。 是否使用join,看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样。
选择驱动表
- 如果是
Index Nested-Loop Join(NLJ)
算法,应该选择小表做驱动表; - 如果是
Block Nested-Loop Join(BNL)
算法:- 在join_buffer_size足够大的时候,是一样的;
- 在join_buffer_size不够大的时候(这种情况更常见),应该选择小表做驱动表。
“小表”的定义
在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
join语句怎么优化
Multi-Range Read优化(MRR),优化的主要目的是尽量使用顺序读盘。
MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。
大表join操作虽然对IO有影响,但是在语句执行结束后,对IO的影响也就结束了。但是,对Buffer Pool的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。young区域的数据页没有被合理地淘汰。
BNL转BKA
Batched Key Acess(BKA)
算法:从表t1里一次性地多拿些行出来,放到join_buffer
,一起传给表t2
扩展-hash join
业务代码实现
小结
- BKA优化是MySQL已经内置支持的,建议你默认使用;
- BNL算法效率低,建议你都尽量转成BKA算法。优化的方向就是给被驱动表的关联字段加上索引;
- 基于临时表的改进方案,对于能够提前过滤出小数据的join语句来说,效果还是很好的;
- MySQL目前的版本还不支持hash join,但你可以配合应用端自己模拟出来,理论上效果要好于临时表的方案。
文章作者 小叨
上次更新 2020-04-06