[TOC](一文吃透 MySQL countcount(*)、count(1) 和 count(字段) 到底有什么区别)The Begin点点关注收藏不迷路前言在日常开发中COUNT函数绝对是使用频率最高的聚合函数之一。但很多开发者对COUNT(*)、COUNT(1)和COUNT(字段名)的区别一知半解甚至存在误区有人说COUNT(1)比COUNT(*)快有人说用COUNT(主键)最快。本文将通过原理分析、执行计划对比和性能测试彻底讲清楚这三者的本质区别让你在面试和实战中都能游刃有余。1. 基本概念COUNT 函数的作用COUNT 是 MySQL 中的聚合函数用于统计非 NULL 值的记录数。但不同用法的统计规则存在差异写法统计对象是否统计 NULLCOUNT(*)整行记录统计所有行不关心任何字段是否为 NULLCOUNT(1)常量 1统计所有行常量 1 永远不会是 NULLCOUNT(字段名)指定字段不统计该字段为 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. 核心区别一张图看懂COUNT(字段名)是否扫描表中的每一行读取该字段的值字段值为 NULL?跳过不计入计入结果COUNT(1)扫描表中的每一行将常量 1 作为表达式1 永远不为 NULL所有行都计入结果COUNT(*)扫描表中的每一行不关心任何字段的值所有行都计入结果3. 执行计划深度分析3.1 测试环境准备-- 创建测试表10万条数据CREATETABLEtest_count(idINTPRIMARYKEYAUTO_INCREMENT,col1VARCHAR(100),col2INT,col3VARCHAR(200),INDEXidx_col2(col2));-- 插入测试数据约50%的 col2 为 NULLINSERTINTOtest_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;idselect_typetabletypepossible_keyskeyrowsExtra1SIMPLEtest_countindexNULLidx_col2100000Using index-- 2. COUNT(1) 执行计划EXPLAINSELECTCOUNT(1)FROMtest_count;idselect_typetabletypepossible_keyskeyrowsExtra1SIMPLEtest_countindexNULLidx_col2100000Using index-- 3. COUNT(字段) 执行计划EXPLAINSELECTCOUNT(col2)FROMtest_count;idselect_typetabletypepossible_keyskeyrowsExtra1SIMPLEtest_countindexNULLidx_col2100000Using index-- 4. COUNT(无索引字段) 执行计划EXPLAINSELECTCOUNT(col3)FROMtest_count;idselect_typetabletypepossible_keyskeyrowsExtra1SIMPLEtest_countALLNULLNULL100000NULL关键发现COUNT(*)和COUNT(1)会优先使用最小的二级索引COUNT(有索引字段)同样会使用该索引COUNT(无索引字段)只能全表扫描4. 性能对比测试4.1 测试方法-- 开启 profilingSETprofiling1;-- 执行三种 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✅ 二级索引略慢需检查 NULLCOUNT(col3)85❌ 全表扫描最慢无索引4.3 百万级数据测试结论渲染错误:Mermaid 渲染失败: Parse error on line 3: ...性能排序 A[COUNT(*) ≈ COUNT(1)] --| ----------------------^ Expecting SQE, DOUBLECIRCLEEND, PE, -), STADIUMEND, SUBROUTINEEND, PIPE, CYLINDEREND, DIAMOND_STOP, TAGEND, TRAPEND, INVTRAPEND, UNICODE_TEXT, TEXT, TAGSTART, got PS5. 不同存储引擎的差异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_schemaSELECTTABLE_ROWSFROMinformation_schema.TABLESWHERETABLE_NAMEtest_countANDTABLE_SCHEMAtest_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)FROMuserWHEREage18;-- 这个语句有问题吗实际上没问题但容易误用-- 更清晰的写法明确意图SELECTCOUNT(*)FROMuserWHEREage18;SELECTCOUNT(CASEWHENage18THEN1END)FROMuser;6.2 COUNT(DISTINCT …) 的用法-- 统计不重复的非 NULL 值数量SELECTCOUNT(DISTINCTage)FROMuser;-- 统计有多少种不同的年龄不含 NULLSELECTCOUNT(DISTINCTname,age)FROMuser;-- 复合列的去重统计-- 如果想统计去重后的总数含 NULL 处理SELECTCOUNT(DISTINCTIFNULL(age,0))FROMuser;-- 将 NULL 转为 0 参与去重7. 最佳实践与优化建议7.1 选择指南渲染错误:Mermaid 渲染失败: Parse error on line 7: ... UseCountStar[COUNT(*) 极快] Q3 -- 否 -----------------------^ Expecting SQE, DOUBLECIRCLEEND, PE, -), STADIUMEND, SUBROUTINEEND, PIPE, CYLINDEREND, DIAMOND_STOP, TAGEND, TRAPEND, INVTRAPEND, UNICODE_TEXT, TEXT, TAGSTART, got PS7.2 性能优化实战场景1大表 COUNT 很慢怎么办-- 方案1使用近似值允许误差SELECTTABLE_ROWSFROMinformation_schema.TABLESWHERETABLE_NAMEbig_table;-- 方案2维护汇总表精确CREATETABLEtable_row_count(table_nameVARCHAR(100),row_countBIGINT,update_timeDATETIME);-- 使用触发器或定时任务更新场景2带 WHERE 条件的 COUNT 优化-- ❌ 慢函数导致索引失效SELECTCOUNT(*)FROMordersWHEREDATE(create_time)2024-01-01;-- ✅ 快范围查询使用索引SELECTCOUNT(*)FROMordersWHEREcreate_time2024-01-01ANDcreate_time2024-01-02;-- ❌ 慢LIKE 前缀模糊匹配SELECTCOUNT(*)FROMuserWHEREnameLIKE%张三%;-- ✅ 快如果能改为前缀匹配SELECTCOUNT(*)FROMuserWHEREnameLIKE张三%;场景3分页总数优化-- 传统分页总数据量 1000 万翻到第 500 页SELECTCOUNT(*)FROMbig_tableWHEREstatus1;-- 耗时 3 秒SELECT*FROMbig_tableWHEREstatus1LIMIT500000,20;-- 耗时 1.5 秒-- 优化使用缓存或延迟加载总数-- 第一页展示时不查询总数滚动到底部或点击“总条数”时才查询-- 或者使用 Redis 维护总数定时更新7.3 面试常见问答Q1COUNT(*) 和 COUNT(1) 哪个更快在 MySQL 5.7 和 8.0 中没有任何区别。优化器会将对COUNT(1)的处理重写为COUNT(*)最终执行计划完全相同。网上说COUNT(1)更快的说法是早期某些数据库的历史遗留误区。Q2COUNT(主键) 是不是最快恰恰相反主键索引是聚簇索引包含了整行数据叶子节点更大。InnoDB 会优先选择最小的二级索引来计算如果没有任何二级索引才会使用主键索引。所以COUNT(主键)通常比COUNT(*)慢。Q3COUNT(*) 会统计 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点点关注收藏不迷路