本文聚焦数据库中常用的count()
函数,通过对比主流数据库的实现方式,揭示传统计数方法的性能瓶颈,进而介绍达梦数据库独有的 counter 属性及其实现原理,并通过性能测试验证该属性在提升计数效率上的显著效果。
你是一个跨境电商运营,随着公司的单子越来越多,你发现平台系统查询订单总数用时越来越长了。
在电商平台的日常运营中,商品列表页的 “商品总数” 展示、用户中心的 “订单总量” 统计,抑或是后台管理系统中 “数据报表总条数” 查询等场景,都离不开对数据库表记录数量的统计。而这些数量的统计,数据库中常用count()
函数来实现。上面提到的各种场景对数据统计的实时性和效率要求极高,一旦count()
查询耗时过长,就会导致页面加载缓慢、用户等待时间延长,严重影响用户体验。而在数据量达到百万甚至千万级别的大型系统中,不同数据库的count()
函数的性能也各有不同。
💡不同的数据库中,count( )有着不同的执行过程
MySQL 中count()
的处理逻辑随存储引擎不同而变化。MyISAM 存储引擎在表结构中维护一个独立的计数器字段,使得count(*)
可直接读取计数器值并返回结果,这仅限无 WHERE
条件的查询。InnoDB 存储引擎不依赖预设计数器,count()
执行时需遍历数据记录;当表存在索引时,会优先通过遍历索引树完成计数;无索引时则执行全表扫描逐行统计。
Oracle 的count()
执行流程由优化器生成的执行计划决定。优化器会根据表的索引分布、数据量等统计信息选择执行路径,当存在可用索引时,会通过扫描索引而非全表来完成计数操作。其计数范围包含所有非空记录,即排除所有列值均为 NULL 的行。
PostgreSQL 中 COUNT()
的实现核心依赖于其 MVCC(多版本并发控制)机制,该机制要求统计时必须逐行检查数据的可见性,无法像 MySQL 的 MyISAM 引擎那样直接读取预存的全局行数。具体执行时,优化器会根据表结构和索引选择以下扫描方式:
全表扫描(Seq Scan):无合适索引时,强制遍历所有数据块,逐行检查可见性并计数。
索引扫描(Index Scan):若存在索引(如主键),优先扫描索引条目,但仍需回表检查行的可见性。
索引覆盖扫描(Index-Only Scan):仅当索引包含所有查询列,且对应的数据块在 Visibility Map(由 VACUUM 维护)中标记为“全可见”时,直接统计索引条目数而无需回表。
💡COUNT(*)
与 COUNT(1)
在 PostgreSQL 中被优化为完全等效的执行计划,均统计所有行且不检查具体列值,具体参考文章:理解 PostgreSQL 的 count 函数的行为 | Zhiya’s Blog
直接拿结果
对比上面各家数据库的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
语句直接进行修改。
摘自:数据定义语句 | 达梦技术文档
设计DM8中开关COUNTER属性的对照测试,直观的体会两者异同。
CPU:i7-11800H
内存:16G
数据库:DM8 单机
SQLark客户端:V3.5
Jmeter:5.6.3
准备两个测试表,均有100w条数据,分别在启用和未启用 counter 属性的情况下,对测试表执行count(*)
操作,使用jmeter分别执行 10000 次。
Jmeter线程属性设置
-- 表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);
-- 快速插入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
-- 测试 WITH COUNTER
SELECT COUNT(*) FROM T_COUNTER; -- 预期:<1ms
-- 测试 WITHOUT COUNTER
SELECT COUNT(*) FROM T_NO_COUNTER; -- 预期:5~10ms (全表扫描)
WITH COUNTER
💡此处的0毫秒意为执行时间小于1毫秒。
WITHOUT COUNTER
WITH COUNTER
WITHOUT COUNTER
数据量增大,差距无疑更大。
WITH COUNTER
1 NSET2: 结果集收集
2 PRJT2: 投影计算表达式
3 FAGR2: 快速聚集 (count(*)直接取值)
关键节点 FAGR2
(快速聚集)
作用:当表存在内置计数器(如元数据记录总行数)时,直接读取计数器值,无需扫描任何数据。
触发条件:查询为 COUNT(*)
或基于索引的 MAX/MIN
,且无 WHERE
过滤条件。
性能:O(1) 时间复杂度,与表大小无关,速度极快。
WITHOUT COUNTER
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 |
二级索引扫描 | 线性扫描 |
本文先阐述了数据库中count()
函数的重要性及性能影响,通过电商等场景引出对高效计数的需求。接着对比了 MySQL、Oracle、PostgreSQL 等主流数据库count()
的实现方式。随后介绍达梦数据库 counter 属性,包括其定义、实现原理。性能测试结果进一步验证了 counter 属性在提升计数效率和降低资源消耗上的效果。
文章
阅读量
获赞