MySQL:count(1)与count(*)有什么区别,深分页问题

前言

在MySQL中使用统计count函数时,count(1)与count()有说明区别,那个性能更好?我下意识的认为count( ) 效率最差,应为他要进行全表扫描。事实却时相反,这究竟是为什么,count底层函数原理是什么呢?

今天这篇文章来帮助大家答疑解惑

那种count性能最好

结论:

count是什么

count是一个聚合函数,函数的参数可以是字段名,其他表达式,该函数的作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录是多少个

假设 count() 函数的参数是字段名,如下:

sql 复制代码
select count(name) from t_order;

这条语句是统计「 t_order 表中,name 字段不为 NULL 的记录」有多少个。也就是说,如果某一条记录中的 name 字段的值为 NULL,则就不会被统计进去。

再来假设 count() 函数的参数是数字 1 这个表达式,如下:

sql 复制代码
select count(1) from t_order;

这条语句是统计「 t_order 表中,1 这个表达式不为 NULL 的记录」有多少个。

1 这个表达式就是单纯数字,它永远都不是 NULL,所以上面这条语句,其实是在统计 t_order 表中有多少个记录

count(主键字段)执行过程是怎么样的

在经过count函数之前,MySQL的service层会维护一个名叫count的变量。

service层会循环像innode读取一条记录,如果count函数指定的参数部位null,于是将count变量+1,知道符合查询的全部记录被读完,就退出循环。最终将count变量的值发给客户端

innodb是通过b+树来保存记录的,根据索引可以分类成:聚簇索引和二级索引。

用下面这条语句作为例子:

sql 复制代码
//id 为主键值
select count(id) from t_order;

如果表中没有二级索引只有主键索引。那么innodb循环遍历主键索引,将读取到的记录返回给service层,让后读取记录中的id值,就会用id值来判断是否为null,如果不为null,将count变量+1。

但是,当表中有二级索引时,innodb循环遍历的对象就不是主键索引,而是二级索引。

因为相同数量的二级索引记录可以比主键索引记录占用更少的存储空间,所以二级索引树比主键索引树小,于是,遍历二级索引的I/O成本小,所以优化器选择二级索引。

count(1) 执行过程

用下面这条语句作为例子:

sql 复制代码
select count(1) from t_order;

如果表里只有主键索引,没有二级索引时。

那么,InnoDB 循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值 ,因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。

可以看到,count(1) 相比 count(主键字段) 少一个步骤,就是不需要读取记录中的字段值,所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。

不过需要注意,上面这个结论只适用于「表里没有任何二级索引、只能扫聚簇索引」这种场景 。如果表上存在二级索引,优化器会把 count(主键字段) 也改写成扫描 key_len 最小的二级索引(参见后文),此时 count(1) 和 count(主键字段) 的执行过程完全一致,性能上没有差异。

但是,如果表里有二级索引时,InnoDB 循环遍历的对象就是二级索引了。

count(*) 执行过程

看到 * 这个字符的时候,是不是大家觉得是读取记录中的所有字段值?

对于 select * 这条语句来说是这个意思,但是在 count() 中并不是这个意思。
count(*) 其实等于 count(0),也就是说,当你使用 count(
) 时,MySQL 会将 * 参数转化为参数 0 来处理。

所以,count(*) 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。

count(字段) 执行过程

count(字段) 的执行效率相比前面的 count(1)、 count(*)、 count(主键字段) 执行效率是最差的。

用下面这条语句作为例子:

sql 复制代码
// name不是索引,普通字段
select count(name) from t_order;

对于这个查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。

小结

count(1)、 count()、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行 count(1)、 count(
)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

如何优化count(*)?

  1. 近似值
    如果业务对于统计个数不需要很精确,比如搜索引擎搜索干建瓷的时候,给出的搜索结果条数是一个大概值。
    我们可以使用show table status或者explain命令来表示估算。
    explain命令效率很高,因为他并不胡正真的去查询。
  2. 额外表保存计数值
    如果像获得精确的获取表的记录总数,我们可以将计数值保存到单独的一张计数表中。
    当在数据表插入一条记录的同时,将计数表的计数字段+1.
    需要额外维护这张计数表。

MySQL分页查询两种limit执行过程

基于主键索引的limit执行过程

sql 复制代码
select * from page order by id limit 0, 10;

service层调用innodb的接口,在innodb里的主键索引中获取第0-10条完整行数据,一次返回给service层,并放到service层的结果集中,返回给客户端。

sql 复制代码
select * from page order by id limit 6000000, 10;

offset改为了6000000,会在innodb里的主键索引中获取到第0-6000000+10条整行数据,返回给service层后根据offset值挨个抛弃,只留下最后main的size条(10条数据)。

可以看出,当offset非0时,server层会从引擎层获取到很多无用的数据,而获取的这些无用数据都是要耗时的。

mysql查询中 limit 1000,10 会比 limit 10 更慢。原因是 limit 1000,10 会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。

因为前面的offset条数据最后都是不要的,所以我们可以将sql语句修改成下面这样:

sql 复制代码
select * from page  where id >=(select id from page  order by id limit 6000000, 1) order by id limit 10;

先执行查询语句select id from page order by id limit 6000000,1,这个操作,将在innodb中的主键索引中获取到6000000+1条数据,然后server层会抛弃前6000000条,只保留最后一条数据的id。

但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。

基于非主键索引的limit执行过程

sql 复制代码
select * from page order by user_name  limit 0, 10;

server层会调用innodb的接口,在innodb里的非主键索引中获取到第0条数据对应的主键id后,回表到主键索引中找到对应的完整行数据,然后返回给server层,server层将其放到结果集中,返回给客户端。

当offset变得非常大时,MySQL会使用全表扫描。

这是因为server层的优化器,会在执行器执行sql语句前,判断下哪种执行计划的代价更小。

很明显,优化器在看到非主键索引的600w次回表之后,摇了摇头,还不如全表一条条记录去判断算了,于是选择了全表扫描。

因此,当limit offset过大时,非主键索引查询非常容易变成全表扫描。

这种情况也能通过一些方式去优化。比如

sql 复制代码
select * from page t1, (select id from page order by user_name limit 6000000, 100) t2  WHERE t1.id = t2.id;

通过select id from page order by user_name limit 6000000, 100。先走innodb层的user_name非主键索引取出id,因为只拿主键id,不需要回表,所以这块性能会稍微快点,在返回server层之后,同样抛弃前600w条数据,保留最后的100个id。然后再用这100个id去跟t1表做id匹配,此时走的是主键索引,将匹配到的100条行数据返回。这样就绕开了之前的600w条数据的回表。

如何解决深分页问题

  1. 子查询优化
    把原本的查询分为两步:先用子查询在二级索引上快速定位其起始id,再用这个id去主键索引取数据
sql 复制代码
-- 原始写法,慢
SELECT * FROM mianshiya
WHERE name = 'yupi'
ORDER BY id
LIMIT 99999990, 10;

-- 优化写法
SELECT * FROM mianshiya
WHERE name = 'yupi'
  AND id >= (
    SELECT id FROM mianshiya
    WHERE name = 'yupi'
    ORDER BY id
    LIMIT 99999990, 1
  )
ORDER BY id
LIMIT 10;

name字段有索引的情况下,子查询只扫描name的二级索引,二级索引只存了name和id,数据量比主键索引小很多。拿到其实id后;再去主键索引取10条完整记录,速度很快。

  1. 游标分页
    每次查询都返回当前页的最大id,下次查询时带上这个id作为起点:
sql 复制代码
-- 第一页
SELECT * FROM mianshiya WHERE name = 'yupi' ORDER BY id LIMIT 10;
-- 假设最大 id 是 100

-- 第二页
SELECT * FROM mianshiya WHERE name = 'yupi' AND id > 100 ORDER BY id LIMIT 10;

利用id>maxId直接过滤,MySQL可以从索引定位到起始位置,不用扫描前面的数据。缺点是只能连续翻页,没法跳到10000页。

  1. 搜索引擎
    把数据同步到elasticsearch,用search_after做深度分页。
相关推荐
苏渡苇2 小时前
5 分钟跑起 Redis(Docker 版)
数据库·redis·缓存·docker·redis入门
m0_493934532 小时前
Go语言中 & 与 - 的本质区别及指针使用详解
jvm·数据库·python
gjc5922 小时前
踩坑案例:容器方式部署的MySQL无法访问?
数据库·mysql
Greyson12 小时前
Redis如何解决哨兵通知延迟问题_优化客户端连接池动态刷新拓扑的订阅监听机制
jvm·数据库·python
bekote2 小时前
笔记|数据库
数据库·笔记
Dream of maid2 小时前
Mysql(8)约束
数据库·mysql
锦轩韶华3 小时前
MySQL 5.1.73(winx64)安装、Navicat 数据库连接测试及简单数据库sql语句操作记录
mysql
程序边界3 小时前
KingbaseES 表空间目录自动创建特性深度解析(下篇)
数据库·oracle
Jul1en_3 小时前
【Redis】Zset类型、命令及应用场景
数据库·redis·缓存