MySQL实战45讲 14

14 | count(*)这么慢,我该怎么办?

在开发系统时,您可能经常需要计算表中的行数,例如交易系统中的变动记录总数。

[En]

When developing a system, you may often need to * calculate the number of rows in a table * , such as the total number of change records in a trading system.

随着系统中记录数越来越多,select count(*) from t 语句执行得也会越来越慢

count(*) 的实现方式

在不同的 MySQL 引擎中,count(*) 有不同的实现方式。

  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;
  • 而 InnoDB 引擎执行 count() 的时候,需要把数据 一行一行*地从引擎里面读出来,然后累积计数。

这里讨论的是没有过滤条件的 count(),如果 加了 where 条件*的话,MyISAM 表也是不能返回得这么快的。

Q:为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?

A:因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表”应该返回多少行”也是不确定的。

举个例子:

假设表 t 中现在有 10000 条记录,我们设计了三个用户并行的会话。

  • 会话 A 先启动事务并查询一次表的总行数;
  • 会话 B 启动事务,插入一行后记录后,查询表的总行数;
  • 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数。

MySQL实战45讲 14

在最后一个时刻,三个会话 A、B、C 会同时查询表 t 的总行数,但拿到的结果却不同。

这和 InnoDB 的事务设计有关系, 可重复读是它默认的隔离级别,在代码上就是通过多版本并发控制,也就是 MVCC 来实现的。 每一行记录都要判断自己是否对这个会话可见,因此对于 count() 请求来说,InnoDB 只好把数据一行一行地读出依次判断, 可见的行才能够用于计算”基于这个查询”的表的总行数*。

在执行 count(*) 操作时的优化

InnoDB 是 索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以, 普通索引树比主键索引树小很多。对于 count() 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此, MySQL 优化器会找到最小的那棵树来遍历*。

在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。

Q:TABLE_ROWS 能代替 count(*) 吗?

A:show table status 命令输出结果是 TABLE_ROWS 。但是实际上,TABLE_ROWS 是从采样估算得来的,因此它很不准。 所以,show table status 命令显示的行数也不能直接使用。

小结

  • MyISAM 表虽然 count(*) 很快,但是不支持事务;
  • show table status 命令虽然返回很快,但是不准确;
  • InnoDB 表直接 count(*) 会遍历全表,虽然结果准确,但会导致性能问题。

如果你现在有一个页面,经常显示交易系统的操作记录总数,你只能自己数。

[En]

If you now have a page that often shows the total number of operating records of the trading system, you can only count it yourself.

自己计数的方法以及优缺点

用缓存系统保存计数

可以 用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1。

通过这种方式,读取和更新操作都很快。

[En]

In this way, both read and update operations are fast.

存在问题:Redis 的数据不能永久地留在内存里,缓存系统可能会丢失更新

解决方法:找一个地方把这个值定期地持久化存储起来。

存在问题:即使持久话存储, 仍然可能丢失更新。试想如果刚刚在数据表中插入了一行,Redis 中保存的值也加了 1,然后 Redis 异常重启了,重启后你要从存储 redis 数据的地方把这个值读回来,而刚刚加 1 的这个计数操作却丢失了。

解决方法:Redis 异常重启以后,到数据库里面单独执行一次 count(*) 获取真实的行数,再把这个值写回到 Redis 里就可以了。异常重启毕竟不是经常出现的情况,这一次全表扫描的成本,可以接受。

存在问题:即使 Redis 正常工作,这个值还是逻辑上 不精确的

假设存在一个页面,要显示 操作记录的总数,同时还要显示 最近操作的 100 条记录。那么,这个页面的逻辑就需要先到 Redis 里面取出计数,再到数据表里面取数据记录。

可能存在两种情况:

  1. 一种是,查到的 100 行结果里面有最新插入记录,而 Redis 的计数里还没加 1;
  2. 另一种是,查到的 100 行结果里没有最新插入的记录,而 Redis 的计数里已经加了 1。

情况1:

MySQL实战45讲 14

会话 A 是一个插入交易记录的逻辑,往数据表里插入一行 R,然后 Redis 计数加 1;会话 B 就是查询页面显示时需要的数据。

在 T3 时刻会话 B 来查询的时候,会显示出新插入的 R 这个记录,但是 Redis 的计数还没加 1。这时候,就会出现数据不一致。

情况2:

MySQL实战45讲 14

会话 B 在 T3 时刻查询的时候,Redis 计数加了 1 了,但还查不到新插入的 R 这一行,也是数据不一致的情况。

在并发系统里面,我们是无法精确控制不同线程的执行时刻的,因为存在图中的这种操作序列,所以,我们说 即使 Redis 正常工作,这个计数值还是逻辑上不精确的

两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。

在数据库保存计数(优)

这个计数直接放到数据库里单独的一张计数表 C 中

解决了崩溃丢失的问题,InnoDB 是支持崩溃恢复不丢数据的。

要解决计数不准确的问题,因为交易、存在是看不见的,所以在逻辑上是一致的。

[En]

To solve the problem of inaccurate counting, because the transaction, the existence is invisible, it is logically consistent.

MySQL实战45讲 14

虽然会话 B 的读操作仍然是在 T3 执行的,但是因为这时候更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见。因此,会话 B 看到的结果里, 查计数值和”最近 100 条记录”看到的结果,逻辑上就是一致的。

不同的 count 用法

Q:在 select count(?) from t 这样的查询语句里面,count(*)、count(主键 id)、count(字段) 和 count(1) 等不同用法的性能,有哪些差别?

A:

count() 的语义

count() 是一个 聚合函数对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。

所以,count()、count(主键 id) 和 count(1) 都表示 返回满足条件的结果集的总行数;而 count(字段),则表示 返回满足条件的数据行里面,参数”字段”不为 NULL 的总个数*。

分析性能差别的原则

  1. server 层要什么就给什么;
  2. InnoDB 只给必要的值;
  3. 现在的优化器只优化了 count(*) 的语义为”取行数”,其他”显而易见”的优化并没有做。

对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表, 把每一行的 id 值都取出来,返回给 server 层。 server 层拿到 id 后,判断是不可能为空的,就按行累加。

对于 count(1) 来说,InnoDB 引擎遍历整张表,但 不取值server 层对于返回的每一行,放一个数字”1″进去,判断是不可能为空的,按行累加。

单看这两个用法的差别的话,count(1) 执行得要比 count(主键 id) 快。因为 从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作

对于 count(字段) 来说

  1. 如果这个”字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
  2. 如果这个”字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。

也就是前面的第一条原则,server 层要什么字段,InnoDB 就返回什么字段。

但是 count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加。

Q:优化器就不能自己判断一下吗,主键 id 肯定非空啊,为什么不能按照 count(*) 来处理

A:MySQL 专门针对这个语句进行优化,也不是不可以。但是这种需要专门优化的情况太多了,而且 MySQL 已经优化过 count(*) 了,直接使用这种用法就可以了。

结论:

按照效率排序的话, count(&#x5B57;&#x6BB5;)< count(&#x4E3B;&#x952E; id)< count(1)count(*),所以我建议你,尽量使用 count(*)

Q:先前用事务来确保计数准确。由于事务可以保证中间结果不被别的事务读到,因此修改计数值和插入新记录的顺序是不影响逻辑结果的。但是,从并发系统性能的角度考虑,你觉得在这个事务序列里,应该先插入操作记录(insert into t),还是应该先更新计数表(update cnt_t)呢?

A:

从并发系统性能的角度来看,应该先插入操作记录,然后再更新盘点表。

[En]

From the point of view of concurrent system performance, the operation record should be inserted first, and then the counting table should be updated.

  • 更新COUNT表涉及行锁竞争首先插入和更新可以最大限度地减少事务之间的锁等待,并提高并发性。
    [En]

    updating the count table involves row lock competition. Inserting and updating first can minimize lock waiting between transactions and improve concurrency.*

PS:计数表保存了多个业务表的计数值不会导致行锁等待。

使用一个计数表来记录多个业务表的行数,并且一定会在表名段中添加唯一的索引。类似于以下内容的表结构

[En]

Use a count table to record the number of rows of multiple business tables, and it is sure to add a unique index to the table name field. A table structure similar to the following

CREATE TABLE rows_stat (
  table_name varchar(64) NOT NULL,
  row_count int(10) unsigned NOT NULL,
  PRIMARY KEY (table_name)
) ENGINE=InnoDB;

在更新计数表的时候,一定会传入where table_name=$table_name,使用主键索引, 更新加行锁只会锁在一行上而在不同业务表插入数据,是更新不同的行,不会有行锁。

Original: https://www.cnblogs.com/ydssx7/p/16517122.html
Author: ydssx
Title: MySQL实战45讲 14

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/505152/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • MySQL事务

    事务用于保证数据的一致性,由一组相关的dml语句组成,该组的dml语句要么全部成功后提交,要么全部失败。进行一个事物操作时,也可以防止其它用户修改表的数据。 举个例子:转账这个操作…

    数据库 2023年6月16日
    071
  • SqlSessionFactory工具类抽取

    多次SqlSessionFactory创建对象问题解决 SqlSessionFactory工具类抽取 问题描述: 当我们多次使用SqlSessionFactory创建并获取对象时会…

    数据库 2023年6月16日
    067
  • JWT+SpringSecurity登录和权限管理

    一、什么是JWT 说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别…

    数据库 2023年6月6日
    086
  • nexus3搭建私有Docker仓库

    Nexus3: version: "3" services: nexus: image: sonatype/nexus3:3.33.0 volumes: – /…

    数据库 2023年6月9日
    060
  • 教师节我用Python做了个学生点名系统送给老师当礼物,这回毕业稳了

    今年教师节前夕,我特意用Python做了个学生点名系统,非常好用,送给各科老师、辅导员当节日礼物,老师们都喜滋滋,说平常逃课就原谅我了,我心想,这次毕业应该不是问题了~ 本文背景 …

    数据库 2023年6月14日
    076
  • 详解 Serverless 架构的 6 大应用场景

    Serverless 架构将成为未来云计算领域重要的技术架构,将会被更多的业务所采纳。进一步深究,Serverless 架构在什么场景下有优秀的表现,在什么场景下可能表现得并不是很…

    数据库 2023年6月14日
    078
  • centos下安装myrocksdb

    承接上一篇,https://www.cnblogs.com/lunyu/p/10190364.html 。编译安装myrocks的整个过程,特别是第2步和第7步,让人冗长难耐。因此…

    数据库 2023年6月14日
    0103
  • 链表(Java)实现

    链表 先给出自定义的list接口,后面几种链表的实现了该接口 public interface List { //统计顺序表元素个数 int size(); //判断顺序表是否为空…

    数据库 2023年6月16日
    077
  • 面试必问之 ConcurrentHashMap 线程安全的具体实现方式

    作者:炸鸡可乐原文出处:www.pzblog.cn 一、摘要 在之前的集合文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环的线上故障! 既然在多线程环境下…

    数据库 2023年6月14日
    089
  • 基于PHP7.2+MySQL5.7的回收租凭系统

    likeshop回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 系统支持智能评估回收价格,后台调整最终回收价,用户同意回收后系统即刻放款,用户微信零钱提现。支持在线…

    数据库 2023年6月14日
    080
  • windows和乌班图使用固定的ip地址

    windows设置固定的ip地址:查看网上的方法很多人说修改无线网卡的配置:自动获取ip—-》使用下面的IP地址这样修改以后无法使用wifi上外网但是确实可以添加一个固…

    数据库 2023年6月11日
    0108
  • Ajax

    AJAX(Asynchronous Javascript And Xml) 传统请求及缺点 传统的请求都有哪些? 直接在浏览器地址栏上输入URL。 点击超链接 提交form表单 使…

    数据库 2023年6月14日
    092
  • Asp.NET core/net 5接口返回实体含有long/int64的属性序列后最后几位变为0的解决

    Asp.NET core /net 5接口返回实体含有long/int64的属性时,序列后最后几位变为0的。 不得不吐槽一下MS,这种事还有问题,NND。 解决方案在startup…

    数据库 2023年6月14日
    073
  • 详解Mysql事务隔离级别与锁机制

    一.概述 我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能 就会导致我们说的脏写、 胀读和不可重复读、幻读这些问题。 这些问题的本质…

    数据库 2023年5月24日
    0133
  • 使用REST风格完成MVC前后端分离

    一个具有REST风格项目的基本特征: 使用REST框架实现前后端分离架构,我们需要首先确定返回的JSON响应结构是统一的,也就是说,每个REST请求将返回相同结构的JSON响应结构…

    数据库 2023年6月11日
    0101
  • 新建Github仓库并上传本地代码

    按照Github的教程 Adding a local repository to GitHub using Git 1. 创建空的Github仓库 创建远程仓库 🔗 ,注意不要勾选…

    数据库 2023年6月14日
    0109
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球