COUNT(*) vs COUNT(1):别再吵架了,MySQL 8.0的执行计划早就告诉你了

简介: COUNT(*)和COUNT(1)哪个更快?这个问题在技术论坛上吵了十几年,答案从“一样快”到“COUNT(1)更快”到“COUNT(*)更快”反复横跳。每次MySQL发新版本,就有人拿出来重新测一遍。本文从执行计划、存储引擎、索引选择、优化器行为四个层面,完整拆解5种COUNT写法的语义差异、性能表现和适用场景。结论可能会让你意外——但更重要的是,你会理解为什么COUNT慢、怎么优化、什么场景该用什么写法。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

COUNT(*)和COUNT(1)哪个更快?

这个问题在技术论坛上吵了十几年。我见过最极端的讨论,有人为了争这个在帖子里盖了200多楼。答案从“一样快”到“COUNT(1)更快”到“COUNT(*)更快”反复横跳。

2026年了,这个问题还值得吵吗?

不值得。因为答案早就有了——看执行计划。

但更值得问的问题是:如果你还在纠结这两个,说明你根本不理解COUNT真正的性能瓶颈在哪。

今天从执行计划、存储引擎、索引选择、优化器行为四个层面,把COUNT这件事彻底讲透。

一、COUNT的语义:5种写法的完整拆解

很多人的问题从一开始就问错了。他们问的是“性能”,但连语义都没搞清楚。

COUNT是聚合函数,计算一个SELECT结果集中的行数。但不同写法对“行”的定义完全不同:

COUNT(star)

  • 计算所有行数

  • 统计的是结果集中的每一行,不管列值是什么

  • 写法:COUNT(*)

  • NULL处理:计入所有行

COUNT(常量)

  • 计算所有行数(1是常量,每行都有值)

  • 与COUNT(*)在语义上等价

  • 写法:COUNT(1)、COUNT(42)、COUNT('hello')

  • NULL处理:计入所有行

COUNT(列名)

  • 计算指定列中非NULL值的行数

  • 只统计该列不为NULL的行

  • 写法:COUNT(column_name)

  • NULL处理:只计入非NULL的行

COUNT(DISTINCT 列名)

  • 计算指定列中非NULL值的去重后个数

  • 写法:COUNT(DISTINCT column_name)

  • NULL处理:不计入NULL

COUNT(star) OVER()

  • 窗口函数版本,计算当前窗口内的行数

  • 写法:COUNT(*) OVER(PARTITION BY ...)

  • NULL处理:计入所有行

写法 含义 是否统计NULL行
COUNT(*) 所有行数 ✅ 是
COUNT(1) 所有行数 ✅ 是
COUNT(col) 某列非NULL行数 ❌ 否
COUNT(DISTINCT col) 某列去重非NULL值个数 ❌ 否
COUNT(*) OVER() 窗口内行数 ✅ 是

关键结论:COUNT(*)和COUNT(1)在语义上完全等价——都是统计所有行数。它们在MySQL 8.0中的执行计划也完全一样。

二、COUNT的存储引擎原理:为什么InnoDB没有“总行数”?

这个问题是所有COUNT性能问题的根源。

MyISAM引擎:存储表的总行数,COUNT(*)直接返回,O(1)。

InnoDB引擎:不存储总行数,COUNT(*)需要实时扫描数据。因为InnoDB支持MVCC(多版本并发控制),不同事务在同一时刻看到的数据行数可能不同,无法缓存一个“总行数”。

所以在InnoDB里,COUNT永远是一个实时计算操作。

那InnoDB怎么执行COUNT的?

  • 没有WHERE条件时:优化器会选择最小的索引(二级索引通常比聚簇索引小)进行扫描,快速估算行数,可能返回近似值。对于COUNT(*),优化器直接读取索引的元数据,不扫描数据行,所以很快——但这是近似值,不是精确值,且两者在8.0里执行路径相同。

  • 有WHERE条件时:必须根据WHERE条件扫描对应的索引或数据,精确计数。

  • 精确COUNT永远需要扫描行数据:所谓的“快”只是相对于全表扫描而言,本质仍然是扫描操作。

三、执行计划分析:不同COUNT写法的真实差异

在1000万行数据的测试表上(有主键id,有二级索引status,status列有20%的NULL值):

场景1:无WHERE条件

写法 执行计划 扫描对象 优化器行为
COUNT(*) Select tables optimized away 无实际扫描 直接从统计信息读取估算行数
COUNT(1) Select tables optimized away 无实际扫描 同上,与COUNT(*)完全相同
COUNT(id) Select tables optimized away 无实际扫描 同上,主键索引统计信息
COUNT(status) 全索引扫描 status二级索引 实际扫描,统计非NULL行数

关键区别:

  • COUNT(*)、COUNT(1)、COUNT(主键)在无WHERE条件时都走Select tables optimized away,直接从索引统计信息读取,几乎没有性能差异。

  • COUNT(status)必须实际扫描status索引,统计非NULL的行数,需要扫描整个二级索引,是全量精确COUNT中最慢的写法之一(如果该列有大量NULL值,仍需扫描完整索引统计非NULL行数,无法跳过NULL)。

场景2:有WHERE条件

写法 执行计划 扫描对象 关键差异
COUNT(*) WHERE status=1 range status二级索引 只扫描status=1的行,无需回表
COUNT(1) WHERE status=1 range status二级索引 同上,优化器等价改写为COUNT(*)
COUNT(id) WHERE status=1 range status二级索引+回表 需要回表读取id
COUNT(status) WHERE status=1 range status二级索引 与COUNT(*)相同

有WHERE条件时,COUNT(*)和COUNT(1)的执行计划完全相同。 COUNT(列)如果列有索引,性能可能接近;如果列没有索引,则走全表扫描。

COUNT(列名)和COUNT(*)的核心差异在于语义:前者统计非NULL的行数,后者统计所有行数。如果列上有大量NULL值,两者结果可能差很多——但性能差异取决于列是否有索引,而非写法本身。

四、优化器视角:MySQL到底是怎么选索引的?

对于COUNT(*),MySQL优化器的决策逻辑是:

  1. 有WHERE条件:选择WHERE条件中最优的索引进行扫描

  2. 无WHERE条件:在候选索引中选择最小的索引(索引树页数最少的)进行扫描

为什么选最小的索引?因为COUNT(*)只需要统计行数,不需要回表读取完整行数据。二级索引比聚簇索引小得多——二级索引只包含索引列+主键值,聚簇索引包含所有列。

重要结论:COUNT(*)不会扫描聚簇索引(除非没有二级索引),因为二级索引的扫描成本更低。这也是为什么COUNT(*)在InnoDB中不是“全表扫描”——它是“全二级索引扫描”。

五、COUNT慢的5个真正原因

原因1:无索引的WHERE条件

这是最常见的场景:

-- status没有索引
SELECT COUNT(*) FROM orders WHERE status = 'PAID';

这会导致全表扫描(扫描聚簇索引),即使加了COUNT(*)也快不起来。

✅ 解决方案:在WHERE条件列上建索引。

原因2:需要精确计数的超大表

一张1亿行的表,任何精确COUNT都需要扫描大量数据。即使走二级索引,也要扫描上亿行索引数据。

✅ 解决方案:考虑用计数表(单独维护计数器)或Redis缓存。

原因3:WHERE条件中使用了函数或隐式类型转换

-- 错误写法:对索引列用了函数
SELECT COUNT(*) FROM orders WHERE DATE(create_time) = '2026-09-01';

-- 错误写法:隐式类型转换(create_time是字符串)
SELECT COUNT(*) FROM orders WHERE create_time = 20260901;

这些写法都会导致索引失效,走全表扫描。

✅ 解决方案:确保WHERE条件中的列是原始列,类型匹配。

原因4:COUNT(DISTINCT)的排序开销

SELECT COUNT(DISTINCT user_id) FROM orders;

COUNT(DISTINCT)内部需要去重,通常需要排序或哈希操作,对大数据集来说很重。

✅ 解决方案:如果业务对“精确去重”要求不高,考虑使用APPROX_COUNT_DISTINCT()(MySQL 8.0.17+支持)或HyperLogLog。

原因5:行格式过大

如果表中有TEXT、BLOB等大字段,行格式可能是动态的,读取成本更高。

✅ 解决方案:确保COUNT只走二级索引,避免回表。

六、4种COUNT优化方案

方案1:用小二级索引替代主键索引

当表没有WHERE条件时,MySQL选择最小的索引扫描。

如果一张表有主键(聚簇索引)和多个二级索引,优化器会选择叶子节点数量最少的索引。

-- 给一个占用空间小的列建二级索引
CREATE INDEX idx_small ON orders(status);
-- COUNT(*) 会优先扫描 idx_small 而不是主键

方案2:用计数表(Counter Table)

适合频繁计数的业务场景。

CREATE TABLE order_stats (
    stat_date DATE PRIMARY KEY,
    total_orders INT,
    paid_orders INT
);

每次插入/更新订单时,同步更新计数表。查询COUNT时直接读计数表,O(1)。

方案3:用EXPLAIN的估算行数

如果业务允许“近似值”,直接看EXPLAIN的rows字段。

EXPLAIN SELECT * FROM orders WHERE status = 'PAID';
-- rows列显示估算行数

EXPLAIN FORMAT=TREE提供了更详细的估算信息,包括每次操作的代价。

方案4:用分区表降低扫描范围

如果表按时间RANGE分区,COUNT只扫描相关分区。

-- 按月分区后 SELECT COUNT(*) FROM orders WHERE create_time >= '2026-09-01'; -- 只扫描9月分区

当然,分区表有额外的维护成本,需要权衡。

七、总结

COUNT(*) vs COUNT(1)的争论可以停了。它们在MySQL 8.0中执行计划完全相同。

真正值得关注的问题只有三个:

  1. WHERE条件能不能用到索引? → 不能就建索引

  2. 业务能不能接受近似值? → 能就用EXPLAIN估算

  3. 能不能用计数表缓存? → 能就单独维护

把精力放在这三个问题上,比争论COUNT(*)和COUNT(1)哪个快有意义100倍。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
24天前
|
SQL 人工智能 Oracle
VLDB 2026核心议题解读:当负载被AI改写,数据库的内核该往哪走?
国际数据库顶级会议VLDB 2026将“AI Agent时代的数据系统”列为核心议题,数据库研究正在转向“如何让数据被AI Agent理解和使用”。当负载被AI改写,数据库需要重新设计什么?DBA的技能储备需要往哪个方向延伸?
|
26天前
|
SQL 监控 关系型数据库
MySQL索引合并优化器陷阱:为什么复合索引比索引合并快一个数量级?
MySQL优化器有一个“自作聪明”的行为——当单列索引无法完全覆盖查询时,它可能选择索引合并(Index Merge) ,同时使用多个单列索引,把结果集合并起来。听起来很合理对吧?但索引合并有严格的适用条件,用错了比全表扫描还慢——尤其是UNION类型的索引合并,需要对多个结果集去重和排序,代价极高。本文拆解索引合并的3种类型、3个踩坑场景,以及什么时候该用复合索引替代。
|
1月前
|
存储 SQL 容灾
共享存储集群 vs 分布式多副本:同城双活两条技术路线怎么选?
同城双活正在成为金融、政务等核心系统的容灾标配——RPO=0、RTO<30秒。但真正的落地远不止“两个机房各放一套数据库”那么简单。网络延迟的容忍度、脑裂预防机制、同步复制的性能代价、以及故障切换后的数据回滚,每一个环节都可能成为“最后一公里”的绊脚石。本文从容灾架构演进入手,拆解同城双活的核心技术原理、关键挑战与应对方案,并结合同城双中心方案及实测数据进行深度解析。
|
2月前
|
存储 消息中间件 SQL
Redis大Key优化完全指南:三种类型、五种拆分策略、一套渐进式方案
大key是Redis最隐蔽的性能杀手——它不会直接报错,只会让你半夜收到延迟告警、主从断开、请求超时。本文从大key的三种类型出发,拆解String、Hash、Set、ZSet、List五类数据结构的拆分策略,提供渐进式拆分的完整方案,并给出数据结构选型的“防患于未然”建议,帮助读者从“发现大key”走向“根治大key”。
|
27天前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
1月前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
24天前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
20天前
|
缓存 人工智能 关系型数据库
大模型调用成本降62%?语义缓存的阈值与命中率实测
客服机器人上线两周,用户问题高度重复,每次都走完整套RAG,数据库读QPS翻三倍,大模型账单飞涨。文章讲清语义缓存怎么用"向量相似度"代替"字符串相等"去命中重复提问,落地时数据该存哪、相似度阈值怎么定,以及多租户隔离、知识库更新失效、别缓存低质量回答这几个真正的难点。附两周实测:命中率约57%,大模型成本降约62%,命中时首字延迟从3.2秒降到0.45秒。
|
24天前
|
人工智能 Cloud Native 数据库
向量数据库选型实战:从 Embedding、ANN 索引到三条落地路线
本文直击向量数据库本质:不堆概念,不列产品,专讲它“是什么”、三条选型路径(专用库/关系库扩展/云托管)如何取舍,以及开发者落地必须关注的召回率、延迟、更新一致性等真实问题。聚焦RAG实战,强调“先用pgvector跑通再升级”,拒绝盲目上马。
120 0
|
20天前
|
SQL 算法 关系型数据库
MySQL大表DDL锁表避坑指南:COPY、INPLACE、INSTANT三种算法全解析
很多人只知道“Online DDL不锁表”,却不清楚什么场景真的不锁、什么场景照样锁。本文从DDL的三种算法入手,拆解Online DDL的触发条件、锁表场景、以及大表DDL的实战避坑方案。