Advertisement

MySQL 中的 count 你用对了吗?性能对比一览

阅读量:

在MySQL中提供的是MySQL中的COUNT()函数是一种功能强大的统计工具,在各种场景下都能发挥出色的作用!你是否知道这种函数在各个引擎中的具体实现细节存在显著差异?除此之外,在本文中你将会深入探索COUNT()函数的各种应用模式,并发现哪些模式更具优势。最后部分我们将对比采用缓存与数据库两种方法存储计数数据的效果与优劣

首先来看一下这张思维导图,对本文内容有个简单的认识。

count(*) 的实现方式

在多种 MySQL 数据库引擎中,count() 采用了不同的计算机制,在本文中我们重点探讨的是未施加过滤条件的 count().

  • MyISAM引擎将整个数据库表的总记录数持久化地存储于其本地存储设备上。因此,在执行COUNT(*)指令时能够迅速得到结果;而当施加WHERE条件时,则无法像MyISAM那样快速响应。
    • 相比之下,在InnoDB引擎中其COUNT(*)操作则必须依次读取每一行数据并进行累加统计。

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

由于在同一个时间段内同时发起的多个并发查询导致InnoDB表“应返回多少行”的结果也是不确定的。

如下案例所示,最后在同一时刻三个会话查询表t的总行数结果不同。

img

这一特性源于InnoDB的事务设计。Read-Commit(TC)隔离级别是其默认设置。具体实现方式是基于多版本并发控制机制(MVCC)。每条记录都需要判断其是否属于当前对话范围,在执行count(*)等聚合查询时,系统必须逐行读取数据并逐一验证以确保可见性。

虽然InnoDB引擎中的count(*)语句需逐行读取但尽管如此仍进行了查询优化这项技术使数据库系统能够在不影响结果的前提下提高性能效率。InnoDB通过索引组织数据存储其中主键字段通过构建多层指针结构实现快速定位功能从而显著提升了数据检索效率并降低了存储空间占用率。具体而言主键字段通常以单层指针形式存储而次级字段则基于此形成多层指针结构从而实现了更高的查询效率与空间利用率平衡。”

除了运行 count(*) 命令以获取数据行数外,在进行数据库管理时我们还经常调用 show table status 命令用于查询表中的记录总数需要注意的是其结果基于采样计算得出官方文档指出误差范围可能在40%至50%之间因此, show table status 命令所得出的数据同样不可直接引用。

总结

  • 尽管 MyISAM 表在进行 count(``*``) 操作时非常高效。
    • 该命令执行得相当迅速。
    • InnoDB 表中的 count(*) 操作可能会遍历整个表,在保证准确性的同时可能会影响性能。

不同的 count 用法

探讨不同COUNT函数(如全选计数、主键计数及特定字段计数等)在执行效率上的差异所在

count() 作为一个聚合函数,在其处理的结果集中逐行检查每一行数据。当 count 函数的参数非 NULL 时,将最终累计值得出作为结果。

所以,在SQL中,count(*)、count(主键 id) 和 count(1) 分别表示返回满足条件的结果集中所有记录的总数;此外,count(字段)则用于计算在满足条件的数据行中且参数"字段"不为空的情况下的记录总数。

对于 count(主键 id) 来说**,** InnoDB 引擎会对整个表进行扫描**,** 提取出每一条记录的主键 id 值**,** 然后将这些 id 值传递给服务器层处理。服务器端接收到这些 id 值后会检查它们是否为空**,** 然后逐行求和计算总数。

在存在多棵普通索的情况下,默认情况下系统会根据优化策略选择其中一棵来执行查询。count(主键 id) 无法通过主键索引来访问数据的原因在于普通的数据存储结构相较于主键数据结构在存储规模上更为有限。

对于 count(1) 函数而言 ,InnoDB 引擎会扫描整个表而不返回具体的数值信息 。在 server 层中会为每条返回的结果行插入一个数字 '1' ,从而确保结果集长度能够正确反映数据量

仅凭这两个用法的差异可以看出两者的不同。\n\n在性能上来看, count(1)比count(主键id)快得多。\n\n由于引擎返回id的过程涉及解析数据行并进行字段值复制操作

对于 count(字段) 来说:

  1. 如果该字段被定义为非空值的话,则逐行读取该字段的值并确保不出现null值的情况后再进行逐行累加;
  2. 如果该字段的定义允许多个null值存在,则在执行过程中会遇到可能的null情况并需进一步确认其具体数值状态后再决定是否进行累加操作;

count(字段) 需要查询出该字段值,只能通过聚簇索引树,所以效率最差。

然而,在使用 count(*) 运算时需要注意的是,并非总是会提取所有字段值。实际上,在这种特殊情况下 count(*) 并不会返回具体的数值结果。相反地,在这种情况下 count(*) 也不会提取所有字段值——它经过了特别的优化处理,并且明确地不返回具体的数值结果。需要注意的是,在这种特殊情况下 count(*) 也不会出现null值——它会将每一条记录逐行相加得到最终的结果值。

主键ID字段必定存在非空值。那么为何数据库的最速搜索算法无法将 count(主键ID) 与 count() 并行处理呢?答案是无需采取此类复杂措施,在大多数情况下无需额外进行重复计算即可满足需求。因此我们建议直接采用 count() 就足够了。

基于以上分析,请您考虑:当依据效率对COUNT函数进行排序时,在以下情况下进行比较:COUNT(字段) < COUNT(主键 id) < COUNT(1) ≈ COUNT()。因此,请优先选择 COUNT()。

某些文章指出使用 count(_) 的效率较低,并认为其他类似的计数方法同样存在问题。其实不然,在讨论性能时应当明确区分 count 函数本身的效率问题以及整体系统的处理能力。针对这一问题,在实际应用中可以通过引入缓存机制来加以优化和改进,在此基础之上再考虑如何实现更加精确的实时查询需求。

用缓存系统保存计数

对于更新很频繁的库来说,你可能会第一时间想到,用缓存系统来支持。

通过使用Redis服务来存储该表的总行数。每当表中插入一行时Redis计数值增加1;每当删除一行Redis计数值减少1。在这样的方式下读取和更新操作均较为高效。但需进一步思考该方案是否存在潜在的问题。

没错,缓存系统可能会丢失更新。

Redis的数据无法永远保留在内存中,因此你需要找到一个位置来定期将该值持久化地存储起来.然而即使这样也会存在更新被遗漏的风险.假设现在向数据库中新增了一条记录,其中对应的计数值增加了1,然后 Redis 发生了异常重启,重启后你需要从存储 Redis 数据的位置读取该值回系统,然而这个计数增加的操作却未能成功保存.

毫无疑问,在这种情况下是有解决方案的。举个例子来说吧:当Redis发生异常重启时,在数据库中进行一次简单的查询操作,在数据库中获取当前数据量。然后将这个结果返回到Redis中作为补充信息。虽然这种情况不常见,但是进行全表扫描所需的成本是可以接受的。

然而,在缓存系统中存储计数的方式不仅仅涉及数据丢失与更新问题。即使Redis运行正常无误,该值仍存在逻辑上的不准确性。

Redis 和 MySQL 是两个主要的数据存储系统,在高并发场景下确保数据一致性得到有效维护。通常的做法是在更新数据库后删除缓存项

我们查询如下两个时序图:

img

在T2时刻时,会话A实施了插入操作;当T3时刻到来时,在缓存中会被会话B读取计数值;从而导致当前获取的计数值与事务完成后再次获取的结果存在差异。

如果对对话A中的计数更新流程与数据插入流程进行重新排列,则能否获得预期效果?

img

此方案仍存在不足之处。尽管在T3时刻时序段会话B能够获取最新的计数信息,并能在后续阶段完成数据同步操作。然而无法实现对即将插入的数据R的处理。

由于 Redis 和 MySQL 基于各自不同的数据存储机制设计,并不具备处理分布式事务的能力,因而无法确保计数的精确性。

在数据库保存计数

根据之前的分析,在采用缓存机制存储计数值时会遇到数据丢失以及计算精度不足的问题。如果我们将这个数值直接放置到数据库中的独立C型计数表中会发生什么呢?

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

利用事务来解决时序2 图中的问题,如下所示:

img

由于MySQL的事务机制结合MVCC特性,在T3时刻时的操作由会话B独立完成,并不会受到会话A的影响。其中,在T4时提交事务后进行的操作变化才会影响到后续的结果。

总结

同一个COUNT(*)函数在不同存储引擎中的具体实现存在差异。相较于直接将计数值存入数据库主表中,使用Redis缓存机制存在一定的局限性。那么,请问:为何将计数值存入Redis并不足以确保与MySQL主表数据的高度一致性?下面我会详细阐述这一现象产生的原因。

Redis和MySQL作为两种不同的数据库系统,在功能上存在显著差异。具体而言, 它们均不具备分布式事务的支持能力, 这一特点直接导致实际应用中难以实现数据的一致性保障. 这一技术限制的存在, 其根本原因在于, 在这种基于行锁的传统设计架构下很难保证数据库操作的一致性和持久性. 相反地, 在采用MySQL等传统关系型数据库时, 则可以通过适当的设计实现这一目标.

全部评论 (0)

还没有任何评论哟~