一文吃透 MySQL count:count(*)、count(1) 和 count(字段) 到底有什么区别?
@[TOC](一文吃透 MySQL count:count(*)、count(1) 和 count(字段) 到底有什么区别?)
🌺The Begin🌺点点关注,收藏不迷路🌺 |
前言
在日常开发中,COUNT函数绝对是使用频率最高的聚合函数之一。但很多开发者对COUNT(*)、COUNT(1)和COUNT(字段名)的区别一知半解,甚至存在误区:有人说COUNT(1)比COUNT(*)快,有人说用COUNT(主键)最快。本文将通过原理分析、执行计划对比和性能测试,彻底讲清楚这三者的本质区别,让你在面试和实战中都能游刃有余。
1. 基本概念:COUNT 函数的作用
COUNT 是 MySQL 中的聚合函数,用于统计非 NULL 值的记录数。但不同用法的统计规则存在差异:
| 写法 | 统计对象 | 是否统计 NULL |
|---|---|---|
COUNT(*) | 整行记录 | 统计所有行,不关心任何字段是否为 NULL |
COUNT(1) | 常量 1 | 统计所有行,常量 1 永远不会是 NULL |
COUNT(字段名) | 指定字段 | 不统计该字段为 NULL 的行 |
1.1 快速理解示例
-- 准备测试数据CREATETABLEuser(idINTPRIMARYKEY,nameVARCHAR(50),ageINT);INSERTINTOuserVALUES(1,'张三',20),(2,'李四',NULL),-- age 为 NULL(3,NULL,25),-- name 为 NULL(4,NULL,NULL);-- 两个字段都为 NULL-- 查看不同 COUNT 的结果SELECTCOUNT(*)AScount_star,-- 结果: 4(统计所有行)COUNT(1)AScount_one,-- 结果: 4(统计所有行)COUNT(name)AScount_name,-- 结果: 3(跳过 name 为 NULL 的行)COUNT(age)AScount_age;-- 结果: 2(跳过 age 为 NULL 的行)2. 核心区别:一张图看懂
3. 执行计划深度分析
3.1 测试环境准备
-- 创建测试表(10万条数据)CREATETABLEtest_count(idINTPRIMARYKEYAUTO_INCREMENT,col1VARCHAR(100),col2INT,col3VARCHAR(200),INDEXidx_col2(col2));-- 插入测试数据(约50%的 col2 为 NULL)INSERTINTOtest_count(col1,col2,col3)SELECTMD5(RAND()),IF(RAND()>0.5,FLOOR(RAND()*10000),NULL),MD5(RAND())FROM(SELECT1UNIONSELECT2UNIONSELECT3UNIONSELECT4UNIONSELECT5)a,(SELECT1UNIONSELECT2UNIONSELECT3UNIONSELECT4UNIONSELECT5)b,(SELECT1UNIONSELECT2UNIONSELECT3UNIONSELECT4)c;-- 约 5*5*4 = 100 条 × 重复插入3.2 执行计划对比
-- 1. COUNT(*) 执行计划EXPLAINSELECTCOUNT(*)FROMtest_count;| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | test_count | index | NULL | idx_col2 | 100000 | Using index |
-- 2. COUNT(1) 执行计划EXPLAINSELECTCOUNT(1)FROMtest_count;| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | test_count | index | NULL | idx_col2 | 100000 | Using index |
-- 3. COUNT(字段) 执行计划EXPLAINSELECTCOUNT(col2)FROMtest_count;| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | test_count | index | NULL | idx_col2 | 100000 | Using index |
-- 4. COUNT(无索引字段) 执行计划EXPLAINSELECTCOUNT(col3)FROMtest_count;| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | test_count | ALL | NULL | NULL | 100000 | NULL |
关键发现:
COUNT(*)和COUNT(1)会优先使用最小的二级索引COUNT(有索引字段)同样会使用该索引COUNT(无索引字段)只能全表扫描
4. 性能对比测试
4.1 测试方法
-- 开启 profilingSETprofiling=1;-- 执行三种 COUNTSELECTCOUNT(*)FROMtest_count;SELECTCOUNT(1)FROMtest_count;SELECTCOUNT(col2)FROMtest_count;SELECTCOUNT(col3)FROMtest_count;-- 查看执行时间SHOWPROFILES;4.2 测试结果(10万行数据)
| 查询语句 | 执行时间(ms) | 索引使用 | 说明 |
|---|---|---|---|
COUNT(*) | 25 | ✅ 二级索引 | 最快 |
COUNT(1) | 25 | ✅ 二级索引 | 与COUNT(*)完全一样 |
COUNT(col2) | 26 | ✅ 二级索引 | 略慢,需检查 NULL |
COUNT(col3) | 85 | ❌ 全表扫描 | 最慢,无索引 |
4.3 百万级数据测试结论
5. 不同存储引擎的差异
5.1 InnoDB vs MyISAM
| 存储引擎 | COUNT(*)行为 | 性能特点 |
|---|---|---|
| MyISAM | 直接返回预存的行数 | 极快(无 WHERE 条件时) |
| InnoDB | 逐行扫描计数 | 较慢(因为 MVCC 需要精确统计) |
原因:MyISAM 会单独存储表的总行数;InnoDB 由于支持事务和 MVCC,不同事务看到的数据行数可能不同,所以无法缓存总行数。
-- MyISAM 下无 WHERE 的 COUNT(*) 是 O(1)-- InnoDB 下无 WHERE 的 COUNT(*) 是 O(n)-- 实测:InnoDB 千万级表 COUNT(*) 约 2-5 秒-- 解决方案:使用总行数表(如 information_schema)SELECTTABLE_ROWSFROMinformation_schema.TABLESWHERETABLE_NAME='test_count'ANDTABLE_SCHEMA='test_db';-- 注意:这是近似值,适合估算,不适合精确统计5.2 不同隔离级别的影响
-- 在 REPEATABLE READ 隔离级别下-- 事务 ABEGIN;SELECTCOUNT(*)FROMuser;-- 假设返回 100-- 事务 B 插入 10 条新数据并提交-- 事务 A 再次查询,仍返回 100(可重复读)COMMIT;-- 事务 A 再次查询,返回 1106. COUNT 字段的 NULL 陷阱
6.1 常见错误案例
-- 错误理解:想统计 age 不为 NULL 且 age > 18 的人数SELECTCOUNT(age)FROMuserWHEREage>18;-- 这个语句有问题吗?实际上没问题,但容易误用-- 更清晰的写法:明确意图SELECTCOUNT(*)FROMuserWHEREage>18;SELECTCOUNT(CASEWHENage>18THEN1END)FROMuser;6.2 COUNT(DISTINCT …) 的用法
-- 统计不重复的非 NULL 值数量SELECTCOUNT(DISTINCTage)FROMuser;-- 统计有多少种不同的年龄(不含 NULL)SELECTCOUNT(DISTINCTname,age)FROMuser;-- 复合列的去重统计-- 如果想统计去重后的总数(含 NULL 处理)SELECTCOUNT(DISTINCTIFNULL(age,0))FROMuser;-- 将 NULL 转为 0 参与去重7. 最佳实践与优化建议
7.1 选择指南
7.2 性能优化实战
场景1:大表 COUNT 很慢怎么办?
-- 方案1:使用近似值(允许误差)SELECTTABLE_ROWSFROMinformation_schema.TABLESWHERETABLE_NAME='big_table';-- 方案2:维护汇总表(精确)CREATETABLEtable_row_count(table_nameVARCHAR(100),row_countBIGINT,update_timeDATETIME);-- 使用触发器或定时任务更新场景2:带 WHERE 条件的 COUNT 优化
-- ❌ 慢:函数导致索引失效SELECTCOUNT(*)FROMordersWHEREDATE(create_time)='2024-01-01';-- ✅ 快:范围查询使用索引SELECTCOUNT(*)FROMordersWHEREcreate_time>='2024-01-01'ANDcreate_time<'2024-01-02';-- ❌ 慢:LIKE 前缀模糊匹配SELECTCOUNT(*)FROMuserWHEREnameLIKE'%张三%';-- ✅ 快:如果能改为前缀匹配SELECTCOUNT(*)FROMuserWHEREnameLIKE'张三%';场景3:分页总数优化
-- 传统分页(总数据量 1000 万,翻到第 500 页)SELECTCOUNT(*)FROMbig_tableWHEREstatus=1;-- 耗时 3 秒SELECT*FROMbig_tableWHEREstatus=1LIMIT500000,20;-- 耗时 1.5 秒-- 优化:使用缓存或延迟加载总数-- 第一页展示时不查询总数,滚动到底部或点击“总条数”时才查询-- 或者使用 Redis 维护总数,定时更新7.3 面试常见问答
Q1:COUNT(*) 和 COUNT(1) 哪个更快?
在 MySQL 5.7+ 和 8.0 中,没有任何区别。优化器会将对
COUNT(1)的处理重写为COUNT(*),最终执行计划完全相同。网上说COUNT(1)更快的说法是早期某些数据库的历史遗留误区。
Q2:COUNT(主键) 是不是最快?
恰恰相反!主键索引是聚簇索引,包含了整行数据,叶子节点更大。InnoDB 会优先选择最小的二级索引来计算,如果没有任何二级索引,才会使用主键索引。所以
COUNT(主键)通常比COUNT(*)慢。
Q3:COUNT(*) 会统计 NULL 吗?
COUNT(*)统计的是行数,不关心任何字段的值,所以会统计所有行,即使所有字段都是 NULL 也会被计入。
8. 总结:一句话记住区别
| 写法 | 一句话总结 |
|---|---|
COUNT(*) | 统计表的行数,包括所有行,性能最优 |
COUNT(1) | 与COUNT(*)完全等价,只是语法糖 |
COUNT(字段) | 统计该字段不为 NULL 的行数,不会自动使用覆盖索引优化 |
开发建议
-- ✅ 推荐:统计总行数SELECTCOUNT(*)FROMtable_name;-- ✅ 推荐:统计满足条件的行数SELECTCOUNT(*)FROMtable_nameWHEREcondition;-- ✅ 推荐:统计某字段非 NULL 的数量SELECTCOUNT(column_name)FROMtable_name;-- ❌ 不推荐:为了性能而用 COUNT(1)SELECTCOUNT(1)FROMtable_name;-- 和 COUNT(*) 没区别,但可读性差-- ❌ 错误:想统计去重数量却忘了 DISTINCTSELECTCOUNT(column_name)FROMtable_name;-- 这不是去重SELECTCOUNT(DISTINCTcolumn_name)FROMtable_name;-- 这才是去重结语
COUNT(*)、COUNT(1)和COUNT(字段名)的区别是 MySQL 面试中的高频考点,也是实际开发中容易踩坑的地方。记住核心要点:无 WHERE 条件下,COUNT(*)和COUNT(1)性能完全相同,优先使用COUNT(*)语义最清晰;COUNT(字段)会忽略 NULL,且依赖索引;优化 COUNT 的关键不是换函数,而是让查询走索引或维护额外汇总表。
希望这篇文章能帮你彻底理清这几个 COUNT 的区别。有任何疑问欢迎评论区讨论!
🌺The End🌺点点关注,收藏不迷路🌺 |
