高性能 MySQL 必备:COUNT(*)、COUNT(1)、COUNT(字段) 的选择法则

前言

在 MySQL 中统计行数是开发中最常见的需求之一,但面对 count(*)count(1)count(字段) 这些写法,很多开发者会感到困惑:它们到底有什么区别?哪种方式性能更好?如何避免踩坑?本文将通过实例来验证这些问题。


一、先看结论:它们到底有什么区别?

测试环境:MySQL 8.0.40,InnoDB 引擎

提示:使用 EXPLAIN SELECT COUNT(...) 可以查看优化器选择的执行计划。

我们从一个简单的例子开始。假设有一张用户表 user,其中包含一个可为空的字段 email

sql 复制代码
CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    email VARCHAR(100) 
);

-- 插入测试数据
INSERT INTO user (name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', NULL),
('王五', 'wangwu@example.com'),
('赵六', NULL);

执行以下查询:

sql 复制代码
SELECT COUNT(*) FROM user;       -- 结果:4(统计所有行数)
SELECT COUNT(1) FROM user;       -- 结果:4(统计所有行数)
SELECT COUNT(email) FROM user;   -- 结果:2(只统计 email 不为 NULL 的行)
SELECT COUNT(id) FROM user;      -- 结果:4(统计主键非 NULL 的行)

📌 核心区别总结

  • COUNT(*) :统计表中所有行的数量,不关心具体列的值是否为 NULL
  • COUNT(1) :与 COUNT(*) 完全等价,性能几乎无差异。
  • COUNT(字段) :只统计该字段不为 NULL 的行数。

二、性能对比:哪种写法更快?

很多人认为 COUNT(1)COUNT(*) 快,或者 COUNT(主键) 是最优选择。但实际情况如何?我们通过实验验证。

实验准备:创建百万级数据表

sql 复制代码
CREATE TABLE big_table (
    id INT PRIMARY KEY AUTO_INCREMENT,
    data VARCHAR(255),
    index idx_data (data)
);

随机生成100w 数据

测试 1:COUNT(*) vs COUNT(1)

sql 复制代码
SELECT COUNT(*) FROM big_table;

SELECT COUNT(1) FROM big_table;

执行查询结果,如下图:

结论 :两者性能几乎完全一致,MySQL 优化器对它们的处理方式相同(COUNT(*) 是 MySQL 内部优先优化的查询模式,优化程度超过 COUNT(1) ,在阅读性上也比更好些)。推荐优先使用COUNT(*)

测试 2:COUNT(*) vs COUNT(主键) vs COUNT(索引字段)

sql 复制代码
-- 使用主键索引
SELECT COUNT(id) FROM big_table;

-- 使用普通索引字段
SELECT COUNT(data) FROM big_table;

-- 全表扫描
SELECT COUNT(*) FROM big_table;

执行查询结果,如下图:

执行SQL EXPLAIN,三条SQL的结果都是一样 ,如下图:

结论 : MySQL对执行查询都有优化,查询索引优化为 (idx_data)二级索引 ,最终结果:COUNT(*) > COUNT(主键) > COUNT(索引字段)

  • COUNT(*) 性能最优 :MySQL 对 COUNT(*) 进行了特殊优化,COUNT(*) 不会真的取出所有列的数据,而是会自动选择最优的索引 进行扫描,避免不必要的 IO,同时避免了一些事务和MVCC 机制的部分检查
  • COUNT(主键)COUNT(*) 稍慢:主键索引(聚簇索引)天然包含数据行,统计时需要读取完整的索引结构(包含所有行数据),也可能MySQL可能优化为查询其他的二级索引。
  • COUNT(普通索引) 性能较慢:需要过滤非NULL值,可能需要回表。

三、使用场景:如何正确选择?

场景 1:统计所有行数(如分页总条数)

sql 复制代码
-- ✅ 推荐写法
SELECT COUNT(*) FROM user;

-- ❌ 不推荐(性能无提升,可读性略差一点)
SELECT COUNT(1) FROM user;

场景 2:统计某字段的有效值数量

sql 复制代码
-- 统计有邮箱的用户数量(自动忽略为null的数据)
SELECT COUNT(email) FROM user;  

场景 3:统计去重后的数量

sql 复制代码
-- 统计不重复的邮箱数量
SELECT COUNT(DISTINCT email) FROM user; 

四、常见误区与避坑指南

误区 1:"MyISAM 的 COUNT(*) 一定快"

虽然 MyISAM 会缓存表的总行数,但仅适用于没有 WHERE 条件的情况

sql 复制代码
-- MyISAM 引擎下,瞬间返回(缓存)
SELECT COUNT(*) FROM user;

-- 带上 WHERE 条件后,依然需要实时统计
SELECT COUNT(*) FROM user WHERE name LIKE '张%';

误区 2:"COUNT(主键) 是最优解"

通过前文的测试可见,COUNT(主键) 的性能可能不如 COUNT(*)


五、选择建议

场景 推荐写法 原因
统计所有行数 COUNT(*) 语义清晰,性能最优
统计非 NULL 值数量 COUNT(字段) 自动过滤 NULL
需要最高性能统计和具体业务字段 COUNT(索引字段) 利用索引快速统计
统计去重后的数量 COUNT(DISTINCT) DISTINCT去重

六、总结

  • COUNT(*) 是通用场景的最佳选择,既符合直觉又有良好性能。
  • 避免使用 COUNT(非索引字段),尤其是大表。
  • 不要盲目相信"经验之谈",用 EXPLAIN 分析执行计划才是王道。
相关推荐
Victor3563 分钟前
Redis(78) 如何设置Redis的缓存失效策略?
后端
开心-开心急了4 分钟前
Flask入门教程——李辉 第四章 静态文件 关键知识梳理 更新1次
后端·python·flask
稻草猫.4 分钟前
Java网络编程套接字
java·后端·java-ee·idea
Victor3566 分钟前
Redis(79)Redis的缓存穿透是什么?
后端
绝无仅有8 分钟前
猿辅导Java面试真实经历与深度总结(三)
后端·面试·github
绝无仅有9 分钟前
猿辅导MySQL面试常见问题解析(一)
后端·面试·github
桦说编程8 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
舒一笑8 小时前
大模型时代的程序员成长悖论:如何在AI辅助下不失去竞争力
后端·程序员·掘金技术征文
lang201509288 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
小羊在睡觉9 小时前
golang定时器
开发语言·后端·golang