news 2026/9/17 3:54:54

SQL中count(1)、count(*)与count(列名)的区别及性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL中count(1)、count(*)与count(列名)的区别及性能优化

年初我帮团队复盘一个慢SQL问题,优化完发现执行计划里count(1)被优化器和count(*)处理成了完全一样的东西。但到了count(列名),情况突然不一样了。群里当时吵了一轮:有人说 count(1) 比 count(*) 快,有人说 count(列名) 最快,还有人因为线上统计数字对不上,最后查出来就是 count 写错了列。

这篇文章我想把这三者的区别一次说清楚:优化器内部怎么处理、NULL 值怎么影响结果、真实的数据扫描差异、以及线上踩坑问题怎么定位。适合所有写 SQL 的开发、DBA,也适合正准备面试的人。

1. count(1) 和 count(*) 先说道说道:它俩凭什么被当成一回事

很多初学者看到count(1)会以为这是“数第一列”,看到count(*)以为会先展开所有列,所以网上大量说法是“count(1) 不读列,因此更快”。这个说法在绝大多数数据库里都不成立,而且是目前流传最广的一个误解。

1.1 优化器眼里它俩长得一模一样

先看一段最普通的 SQL:

SELECT COUNT(*) FROM orders; SELECT COUNT(1) FROM orders;

在 MySQL 8.0 中分别执行EXPLAIN,你会发现两条语句的typekeyrowsExtra完全一致。再进一步开启优化器跟踪,可以看到 MySQL 内部在解析阶段就把COUNT(1)重写为COUNT(*)。换句话说,1这个常量根本没有参与任何真正意义上的“取值”,只是占了个位置。

SQL Server 和 Oracle 也一样。SQL Server 中COUNT(1)COUNT(*)会被编译成相同的执行计划:扫描一个索引,统计行数。Oracle 的COUNT(1)也会在优化器层被归一化处理。

这也是为什么你在真正资深的 DBA 嘴里,听不到“count(1) 比 count(*) 快”这个结论。它俩的差异在绝大多数数据库引擎里都不存在。

提示:如果哪天你测出来 count(1) 和 count(*) 耗时有明显差异,优先检查是不是缓存干扰、索引选择变化、或者 SQL 语句复杂到已经触发不同执行计划,而不是把它简单归因于“写法不同”。

1.2 “count(1) 更快”这个说法是从哪来的

这个误解来源很杂,但我个人推测和两个历史背景有关。

第一,Oracle 早期版本中,某些优化模式下对SELECT COUNT(1) FROM t这种写法偶发会走更合适的路径,而COUNT(*)在某些复杂查询里会被解析成“检索所有列”,性能反而不稳定。这个细节被很多人当成普适经验抄走了。

第二,早期很多 ORM 框架自动生成 SQL 时,喜欢用COUNT(1)替代COUNT(*),原因是某些数据库的 SQL 解析器对*的处理有额外开销,常量1不需要解析列列表。框架层这么用,慢慢就演变成了“count(1) 性能更优”的口口相传。

但从现在的 MySQL 8.0、PostgreSQL、SQL Server 2019、Oracle 19c 来看,这个性能差基本不存在。真正应该关心的从来不是1还是*,而是这张表你要统计什么数据、走了什么索引、有没有 WHERE 条件。

2. count(列名) 才是真正的异类:它数的是非空值

如果说 count(1) 和 count(*) 是一对双胞胎,那count(列名)就是那个同父异母的兄弟——长得像,但语义上根本不是一回事。

2.1 NULL 直接决定结果长短

COUNT(列名)统计的是这一列中非 NULL 值的行数。也就是说,如果某一行的该列是 NULL,这行不会被计入。

看个最直观的例子:

CREATE TABLE t_user ( id INT PRIMARY KEY, name VARCHAR(50), phone VARCHAR(20) ); INSERT INTO t_user (id, name, phone) VALUES (1, '张三', '13800000000'), (2, '李四', NULL), (3, '王五', '13900000000');

然后执行:

SELECT COUNT(*), COUNT(1), COUNT(phone) FROM t_user;

结果:

表达式结果
COUNT(*)3
COUNT(1)3
COUNT(phone)2

两张只差一个phone为 NULL,count(phone)就少了 1。很多开发第一次看到这个结果都愣一下,说明平时写 count 时根本没想过可空性。

2.2 聚合函数对 NULL 的“无视规则”

不只是 count,几乎所有聚合函数都有类似行为。SUM(列名)遇到 NULL 会当 0 处理,AVG(列名)会忽略 NULL 行再求平均,COUNT(DISTINCT 列名)同样会忽略 NULL。这是 SQL 标准里的默认语义。

所以如果业务上希望“统计所有行中该列有效值有几个”,用 count(列名) 完全没问题。但如果希望“统计表里一共有多少行”,却因为某些行的列值是 NULL 导致结果对不上,那就是 SQL 语义和业务口径不匹配了。

还有一个更隐蔽的坑:COUNT(列名)配合GROUP BY时,如果分组列本身也有 NULL,那么这个组在结果中通常不会出现。比如SELECT dept_id, COUNT(emp_id) FROM t_emp GROUP BY dept_id,当dept_id为 NULL 时,这一组在大多数数据库里会被直接忽略,不在结果集中展示。这又是一个容易在报表里造成数据缺失的点。

2.3 它和 count(distinct 列名) 的区别也别搞混

COUNT(DISTINCT 列名)在 count(列名) 基础上还要做去重,开销更大,含义也不同。

还是上面那张表:

SELECT COUNT(DISTINCT phone) FROM t_user;

结果是 2,因为有两个非空且不同的 phone。如果列里出现重复值,比如两条记录都填了同一个手机号,COUNT(phone)会返回 2,COUNT(DISTINCT phone)只会返回 1。这俩一个数“有效值数量”,一个数“不同有效值数量”,别在指标口径上混用。

3. 实测数据说话:100万、500万行下不同写法的真实差距

有段时间我在帮朋友调一张订单报表,顺手做了个针对 count 的测试。这里把过程和结果分享出来,作为判断参考。

3.1 我的测试环境与用例设计

测试环境:

  • MySQL 8.0.28,InnoDB 引擎
  • 单表结构模拟订单表,约 500 万行数据
  • 主键order_idBIGINT
  • order_statusTINYINT,其中约 12% 为 NULL
  • 二级索引idx_status(order_status)

测试 SQL:

SELECT COUNT(*) FROM orders; SELECT COUNT(1) FROM orders; SELECT COUNT(order_id) FROM orders; SELECT COUNT(order_status) FROM orders;

每个 SQL 执行前都清空缓存,连续执行 5 次取中间值,避免偶然抖动。

3.2 三种写法耗时与执行计划

实测耗时大致如下:

SQL耗时(约)执行计划扫描
COUNT(*)0.72s全扫描二级索引 idx_status
COUNT(1)0.72s全扫描二级索引 idx_status
COUNT(order_id)0.75s全扫描主键索引/聚簇索引
COUNT(order_status)1.10s全扫描二级索引 idx_status

注意到一个反直觉的点:count(order_status)明明扫描的是和count(*)相同的二级索引,但耗时明显更高。原因是这一列允许 NULL,引擎在读每一行索引记录时,还需要额外判断对应字段是否为 NULL,这个判断在 500 万行级别上会被放大。

再看执行计划,type都是indexkey可能是主键或二级索引。InnoDB 下count(*)不指定 WHERE 时,优化器会优先选择一个最小的辅助索引来扫描,因为辅助索引比聚簇索引小,一次扫描的页更少,IO 成本更低。而count(order_id)如果 order_id 恰好是主键,优化器可能选择聚簇索引扫描,数据页更多,耗时反而略高。

3.3 为什么 InnoDB 下 count(*) 也不是“免费午餐”

很多从 MySQL 5.5 时代过来的开发者,习惯把 count(*) 当成 O(1) 操作,因为在 MyISAM 引擎里,表总行数直接存在表的元数据中,不带 WHERE 的COUNT(*)秒回。

但 InnoDB 不是这样。InnoDB 要支持事务和 MVCC,不同事务看到的数据版本不同,所以它无法像 MyISAM 那样缓存一个全局行数。即使没有任何 WHERE,COUNT(*)也必须真实扫描索引,逐行统计当前事务可见的行。

这也是为什么千万级甚至亿级表上做SELECT COUNT(*) FROM t会明显卡顿。它不是一个取元数据动作,而是一次真实的索引全扫描。如果你在做的是大表实时计数,建议趁早换方案,而不是纠结改写成 count(1)。

4. 一次由 count(列名) 引发的线上统计偏差排查记录

理论说多了容易飘,我讲一个自己踩过的线上 bug。那次问题不大,但排查链路很有代表性,希望帮你避开同类坑。

4.1 场景描述:月度报表数量对不上

某天运营反馈:后台“本月已完成订单数”和财务手动导出的数据差了几百单。后台报表使用的 SQL 很长,我最后定位到核心计数语句长这样:

SELECT COUNT(pay_time) FROM order_info WHERE order_status = 'COMPLETED' AND pay_time >= '2024-10-01' AND pay_time < '2024-11-01';

逻辑看起来没毛病:统计已完成订单里,支付时间在 10 月的订单数。财务导出的口径也是已完成订单。

但关键在于COUNT(pay_time):如果某笔已完成订单的pay_time是 NULL,这一行根本不会被计入。你猜业务上会不会出现已完成订单但没有支付时间的记录?

会。比如线下补录订单、历史数据迁移、异常状态的兼容处理,都可能导致order_status='COMPLETED'pay_time为空。运营看到的结果是“少了几百单”,本质是COUNT(pay_time)把 NULL 值全部排除了。

4.2 完整排查链路:从 SQL 到数据再到业务

第一步,我先确认计数差异是否稳定。让运营筛出几个特定订单编号,发现这些订单在后台详情页能查到,状态确实是已完成,但支付时间展示为空。

第二步,我单独跑了一条 SQL:

SELECT COUNT(*) , COUNT(pay_time) FROM order_info WHERE order_status = 'COMPLETED' AND pay_time >= '2024-10-01' AND pay_time < '2024-11-01';

两个值果然对不上,这就坐实了是 NULL 影响。

第三步,去查为什么这些订单没有 pay_time。看完数据迁移脚本发现,早期系统从老库迁移时,一部分已完成订单没迁移支付时间字段,补脚本时也没做数据校验。这属于历史遗留数据质量坑。

4.3 根因与修复:NULL 导致的统计口径问题

修复 SQL 很简单,把COUNT(pay_time)改成COUNT(*)或者COUNT(1),先保证行数和业务口径一致:

SELECT COUNT(*) FROM order_info WHERE order_status = 'COMPLETED' AND pay_time >= '2024-10-01' AND pay_time < '2024-11-01';

如果业务展示层确实只需要展示“有支付时间的订单数”,那我建议在字段语义上当区分处理,比如接口返回两个指标:总完成订单数、有支付时间的订单数,避免指标口径来回切换。另外补了一条数据订正任务,把历史 NULL 的 pay_time 按补单记录回填,但回填前先和财务确认了规则。

4.4 这类问题怎么从源头避免

在这之前,我以为 count(列名) 的 NULL 语义是“人尽皆知”的基础知识,但实际统计报表里踩到的人不在少数。根源在于:写报表 SQL 的人默认“业务上不可能出现 NULL”,但数据库里字段是否可空,和业务上是否允许为空,经常不是一回事。

之后我给自己定了几条规矩:

  • 统计行数一律用COUNT(*)COUNT(1),不要用业务字段
  • 确实需要统计“某字段有值的行数”时,在 SQL 注释里显式写明“忽略 NULL 行”
  • 定期跑数据质量校验,重点检查状态为终态但关键时间字段为 NULL 的记录
  • DDL 阶段就该把“此字段是否允许为 NULL”想清楚,默认允许 NULL 是最省事但最容易埋坑的选择

5. 实际项目里到底该怎么选:给出几条能直接用的判断标准

写 SQL 没有银弹,但 count 的选型逻辑其实很清晰。

5.1 选型判断流程

我建议按这个顺序决策:

  1. 如果目标是“查总行数”,不管有没有 WHERE,直接写COUNT(*)。不要写 count(主键),不要写 count(常数列),更不要写 count(可空列)
  2. 如果目标是“查某列非 NULL 行数”,写COUNT(列名),并且在代码评审时让人一眼看懂你的意图
  3. 如果目标是“查某列不重复的非空值数量”,写COUNT(DISTINCT 列名)
  4. 如果目标是“查行数且性能优先”,优先考虑有没有更小的二级索引让优化器选,必要时显式给提示或重建更紧凑的索引

关于优化器选索引的问题,补一个实践细节:当一张表存在多个索引时,InnoDB 的COUNT(*)会选择扫描“最小”的辅助索引,而不是一定选主键。所以如果你发现 count 扫描的 key 不是预期索引,也可以用FORCE INDEX测试,但通常没必要干预,优化器决策在大多数情况下是合理的。

5.2 大数据量下 count 的优化思路

一旦表到了千万行以上,任何形式的COUNT(*)都可能变成慢查询。这时候应该想的是能不能不做实时精确计数。

常见方案有几个:

  • 对历史表按时间分区后,统计时只扫描需要的分区
  • 用 Redis 维护计数,写入时同步累加,适合详情页展示那种“不必绝对准”的场景
  • 用独立计数表,在一个事务里同步更新业务数据和计数器
  • 对允许近似统计的场景,利用EXPLAIN估算行数或使用统计信息采样

其中计数表方案我最常用。它在事务里对计数器行加锁更新,读取时直接查计数表,能在很大程度避免大表的全索引扫描。代价是需要维护额外逻辑,但比每次都扫几百万行索引靠谱多了。

5.3 面试回答模板与常见追问

如果面试问“count(1)、count(*) 和 count(列名) 的区别”,可以按这个思路回答:

先说语义:count(*) 和 count(1) 都是统计满足条件的总行数,不关心具体列值;count(列名) 只统计该列非空值的行数。

再说性能:在 MySQL InnoDB 中,count() 和 count(1) 执行计划基本一致,没有明显的性能差异。count(列名) 如果列可空,会额外做 NULL 判断,可能更慢;如果列是非空且有合适的二级索引,在某些场景下也可能比 count() 快,因为辅助索引更小,扫描页更少。

最后补充引擎差异:MyISAM 可以直接返回不带 WHERE 的 count() 结果,InnoDB 则必须扫描索引。回到 Oracle 和 SQL Server,同样建议把统计行数统一写成 count()。

面试官如果追问“为什么 count() 和 count(1) 没差异”,可以从优化器重写、执行计划一致、索引扫描成本相同这几个角度答。如果追问“count(列名) 什么时候比 count() 快”,可以答“列非空、有二级索引,且二级索引比聚簇索引小的时候”。

最后再分享一个小经验:写 SQL 时,代码评审阶段看到COUNT(1)我不会拦,但看到COUNT(业务字段)而不是COUNT(*)的时候,我会要求写的人说明理由,因为这里藏着太多统计口径问题。真正的线上事故往往不是 1 和*谁快谁慢,而是你把 count 写在了哪一列上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:52:10

AI测试工具兴起,2027年非AI驱动工具淘汰,测试工程师如何转型

最近测试圈子里传得最凶的一件事&#xff0c;就是微软内部文件提到2027年要淘汰所有非AI驱动的测试工具。很多朋友跑来问我&#xff0c;说这是不是意味着我们这帮写脚本、点页面的测试工程师要集体失业了。我的看法比较直接&#xff1a;这份文件更像是一个行业风向标&#xff0…

作者头像 李华
网站建设 2026/9/17 3:51:58

把 CC-Switch 的上游 Key 换成 TaoToken 后,WSL2 里也能跑通 Claude Code

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 3:51:08

Linux卡在emergency mode?fstab的UUID错配是主因

周六早上我远程连家里那台Linux NAS&#xff0c;结果怎么都ping不通。过了一会儿家人拍来一张照片&#xff0c;屏幕停在黑底白字的启动界面&#xff0c;上面明晃晃一行字&#xff1a;“Welcome to emergency mode!”看到这行字我反而松了口气&#xff0c;因为这类故障我处理过太…

作者头像 李华
网站建设 2026/9/17 3:49:03

Spring三级缓存机制:循环依赖与AOP代理全解析

最近有个同事跑来问我&#xff0c;说在智谱清言里搜“三级缓存具体是什么”&#xff0c;AI给讲了一堆Spring源码。他看完还是懵的&#xff0c;就跑来让我用人话再讲一遍。这个题目确实经典&#xff0c;面试问烂了&#xff0c;网上文章也一大把&#xff0c;但能把“为什么非得是…

作者头像 李华
网站建设 2026/9/17 3:48:53

grub> 命令行救援指南:Linux 引导故障修复与预防

开机之后没看到熟悉的桌面或者登录界面&#xff0c;屏幕上顶着一行grub>或者grub rescue>&#xff0c;下面还跟着一句minimal bash-like line editing is supported。第一次遇到的人十有八九会慌&#xff0c;以为系统挂了&#xff0c;其实大部分情况下数据都还在&#xf…

作者头像 李华