注册
从count( )到达梦counter
技术分享/ 文章详情 /

从count( )到达梦counter

LyC_Dd 2025/08/15 64 0 0

1、概述

本文聚焦数据库中常用的count()函数,通过对比主流数据库的实现方式,揭示传统计数方法的性能瓶颈,进而介绍达梦数据库独有的 counter 属性及其实现原理,并通过性能测试验证该属性在提升计数效率上的显著效果。

2、背景

你是一个跨境电商运营,随着公司的单子越来越多,你发现平台系统查询订单总数用时越来越长了。

在电商平台的日常运营中,商品列表页的 “商品总数” 展示、用户中心的 “订单总量” 统计,抑或是后台管理系统中 “数据报表总条数” 查询等场景,都离不开对数据库表记录数量的统计。而这些数量的统计,数据库中常用count()函数来实现。上面提到的各种场景对数据统计的实时性和效率要求极高,一旦count()查询耗时过长,就会导致页面加载缓慢、用户等待时间延长,严重影响用户体验。而在数据量达到百万甚至千万级别的大型系统中,不同数据库的count()函数的性能也各有不同。

3、各家count( )对比

💡不同的数据库中,count( )有着不同的执行过程

3.1 MySQL 的 count( )

MySQL 中count()的处理逻辑随存储引擎不同而变化。MyISAM 存储引擎在表结构中维护一个独立的计数器字段,使得count(*)可直接读取计数器值并返回结果,这仅限无 WHERE 条件的查询。InnoDB 存储引擎不依赖预设计数器,count()执行时需遍历数据记录;当表存在索引时,会优先通过遍历索引树完成计数;无索引时则执行全表扫描逐行统计。

MySQL count( )官方文档

3.2 Oracle 的 count( )

Oracle 的count()执行流程由优化器生成的执行计划决定。优化器会根据表的索引分布、数据量等统计信息选择执行路径,当存在可用索引时,会通过扫描索引而非全表来完成计数操作。其计数范围包含所有非空记录,即排除所有列值均为 NULL 的行。

Oracle count( )官方文档

3.3 PostgreSQL 的 count( )

PostgreSQL 中 COUNT() 的实现核心依赖于其 MVCC(多版本并发控制)机制,该机制要求统计时必须逐行检查数据的可见性,无法像 MySQL 的 MyISAM 引擎那样直接读取预存的全局行数。具体执行时,优化器会根据表结构和索引选择以下扫描方式:

  1. 全表扫描(Seq Scan)​​:无合适索引时,强制遍历所有数据块,逐行检查可见性并计数。

  2. 索引扫描(Index Scan)​​:若存在索引(如主键),优先扫描索引条目,但仍需回表检查行的可见性。

  3. 索引覆盖扫描(Index-Only Scan)​​:仅当索引包含所有查询列,且对应的数据块在 ​Visibility Map​(由 VACUUM 维护)中标记为“全可见”时,直接统计索引条目数而无需回表。

💡COUNT(*)COUNT(1)​ 在 PostgreSQL 中被优化为完全等效的执行计划,均统计所有行且不检查具体列值,具体参考文章:理解 PostgreSQL 的 count 函数的行为 | Zhiya’s Blog

4、counter——达梦数据库的 count(*)实现

直接拿结果

对比上面各家数据库的count()执行过程,我们发现了像 MySQL 的 MyISAM 存储引擎,通过长期维护独立的计数器字段,来达到COUNT(*)效率的提升。达梦数据库同样采用了这种思路——在表内维护一个表行计数器。我们可以通过WITH COUNTER属性来启用表内的表行计数器,同时这个存储属性是默认开启的。

WITH COUNTER:在表上维护当前表内的行数;WITHOUT COUNTER:表上只维护一个非实时的大概的行数;对用户的影响:例如 SELECT COUNT(*) FROM test; 如果表 test 是 WITH COUNTER 属性,服务器直接取行数返回即可,可以快速响应;如果表 test 是 WITHOUT COUNTER 属性,服务器需要先扫描 B 树获取行数返回后才能响应。不同的场景,根据需要灵活选择 COUNTER 属性。WITH COUNTER 属性可以通过 ALTER TABLE 语句修改。若省略该选项,默认是 WITH COUNTER 属性。同时COUNTER属性可以通过alter table语句直接进行修改。
摘自:数据定义语句 | 达梦技术文档

5、测试

设计DM8中开关COUNTER属性的对照测试,直观的体会两者异同。

5.1 测试环境

CPU:i7-11800H

内存:16G

数据库:DM8 单机

SQLark客户端:V3.5

Jmeter:5.6.3

5.2 测试准备

准备两个测试表,均有100w条数据,分别在启用和未启用 counter 属性的情况下,对测试表执行count(*)操作,使用jmeter分别执行 10000 次。

Jmeter线程属性设置

20250808174322image.png

5.2.1 创建测试表

-- 表1:WITH COUNTER(默认属性) CREATE TABLE T_COUNTER ( ID INT PRIMARY KEY, DATA VARCHAR(100) ); -- 表2:WITHOUT COUNTER CREATE TABLE T_NO_COUNTER ( ID INT PRIMARY KEY, DATA VARCHAR(100) ) storage(without counter);

5.2.2 数据初始化

-- 快速插入100万行(分批提交) BEGIN FOR i IN 1..1000 LOOP INSERT INTO T_COUNTER SELECT (i-1)*1000 + ROWNUM, RPAD('X',50) FROM DUAL CONNECT BY LEVEL <= 1000; COMMIT; END LOOP; END; -- 重复同样操作初始化 T_NO_COUNTER

5.3 执行测试

-- 测试 WITH COUNTER SELECT COUNT(*) FROM T_COUNTER; -- 预期:<1ms -- 测试 WITHOUT COUNTER SELECT COUNT(*) FROM T_NO_COUNTER; -- 预期:5~10ms (全表扫描)

5.4 结果分析

5.4.1 单次查询

WITH COUNTER

20250808174021image.png

💡此处的0毫秒意为执行时间小于1毫秒。

WITHOUT COUNTER

20250808174045image.png

5.4.2 Jmeter10000次查询

WITH COUNTER

20250808172918image.png

WITHOUT COUNTER

20250808172850image.png

数据量增大,差距无疑更大。

5.4.3 执行计划

WITH COUNTER

20250808173913image.png

1 NSET2: 结果集收集
2   PRJT2: 投影计算表达式
3     FAGR2: 快速聚集 (count(*)直接取值)

关键节点 FAGR2(快速聚集)​

  • 作用​:当表存在内置计数器(如元数据记录总行数)时,直接读取计数器值,​无需扫描任何数据

  • 触发条件​:查询为 COUNT(*)或基于索引的 MAX/MIN,且无 WHERE过滤条件。

  • 性能​:​O(1) 时间复杂度,与表大小无关,速度极快。

WITHOUT COUNTER

20250808173557image.png

1 NSET2: 结果集收集
2   PRJT2: 投影计算表达式
3     AAGR2: 简单聚集(计算count(*))
4       SSCN: 二级索引全扫描 (INDEX33555499)

SSCN(二级索引扫描)​

  • 对索引 INDEX33555499进行全扫描(btr_scan(1)),逻辑上遍历所有条目。

  • ​成本标记 [104,1000000,0]​:

  • 104:预估CPU代价

  • 1000000:​扫描100万行​(表明表数据量)

  • 0:无磁盘I/O代价(数据在内存)

AAGR2(简单聚集)​​

  • 在 SSCN扫描结果上逐行统计总数(sfun_num(1)表示1个集函数 COUNT(*))。

  • 性能瓶颈​:因需完整扫描索引,耗时为 ​O(n)​,数据量越大越慢。

我们根据执行计划可以做一个最终的对比表格。

机制 执行计划路径 扫描方式 性能级别
WITH COUNTER NSET2 -> PRJT2 -> FAGR2 无物理扫描 ​常量级
WITHOUT COUNTER NSET2 -> PRJT2 -> AAGR2 -> SSCN 二级索引扫描 ​线性扫描

6、小结

本文先阐述了数据库中count()函数的重要性及性能影响,通过电商等场景引出对高效计数的需求。接着对比了 MySQL、Oracle、PostgreSQL 等主流数据库count()的实现方式。随后介绍达梦数据库 counter 属性,包括其定义、实现原理。性能测试结果进一步验证了 counter 属性在提升计数效率和降低资源消耗上的效果。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服