☰
SQL COUNT函数避坑指南:从NULL陷阱到性能优化实战
2026/10/3 14:38:04 网站建设 项目流程

做开发这些年,COUNT 大概是 SQL 里用得最频繁的聚合函数,没有之一。订单统计、用户计数、报表汇总、分页总数,几乎每天都要和它打交道。然而越常用的东西,踩坑的时候越容易让人懵:为什么 JOIN 之后数量莫名其妙翻倍?为什么 COUNT(列名) 比 COUNT(*) 少了好几千?为什么大表上跑一次 COUNT 能让你把咖啡喝完?这些问题背后,其实都是对 COUNT 底层语义和数据库执行机制的理解不到位。这篇文章就把 COUNT 的常见用法、容易翻车的场景、以及性能优化思路一次性讲透,适合刚接触 SQL 的新手,也适合写过很多 SQL 但偶尔被 COUNT 坑一把的从业者。

1. COUNT 到底数的是什么:先分清是行数还是非空值

1.1 COUNT(*) 和 COUNT(列名) 结果不一样?问题基本出在 NULL

我第一次被 COUNT 坑,是统计用户表里有多少人填了手机号。当时直觉写了一行COUNT(phone),结果比总用户数少了 20%。一开始以为是数据有问题,后来查了一下文档才反应过来:COUNT(列名)只统计该列非 NULL 的行数。

为什么会这样?SQL 里 NULL 表达的是“未知”,不是空字符串,也不是 0。聚合函数碰上“未知”时,默认选择直接忽略它,否则对未知值做任何统计都没有意义。这个规则不光是 COUNT 一家,SUM、AVG、MIN、MAX 全都遵循,只是 COUNT 因为使用频率太高,最容易踩到。

举个具体例子。一张 100 行的用户表,phone 字段有 85 行有值,15 行为 NULL,那么:

SELECT COUNT(*) FROM users; -- 返回 100 SELECT COUNT(phone) FROM users; -- 返回 85

这两条语句的语义完全不同:前者问“结果集有多少行”,后者问“这列有多少个非空值”。所以在写统计 SQL 之前,先想清楚一个问题:你到底要数“行”,还是要数“值”。

这里还有个容易忽略的细节:空字符串 '' 不是 NULL。COUNT(phone)会把手动填的空字符串也算进去。如果业务里把手机号留空存成了 '',想排除它就得配合 CASE WHEN:

SELECT COUNT(CASE WHEN phone IS NOT NULL AND phone <> '' THEN 1 END) FROM users;

这一条 SQL 才真正统计了“填了有效手机号”的用户数量。NULL 和空字符串的区别,建议在团队评审和建表规范里就讲清楚,不然后面统计口径会很乱。

1.2 COUNT(1) 和 COUNT(*) 到底谁更快,答案和你想的相反

网上有一种流传很广的说法:COUNT(1) 比 COUNT(*) 快,因为*要取所有列。这个说法在老一辈 DBA 圈里传了很久,但放在今天的主流数据库上,基本是错的。

COUNT(1)的意思是:对每一行计算常量 1,然后统计 1 出现的次数。因为 1 永远非 NULL,所以它和COUNT(*)在语义上完全等价——都是在数结果集的行数。COUNT(0)、COUNT('abc')也一样,只要常量非 NULL,都是数行数。

而COUNT(*)在数据库内部也不会真的去“展开所有列”。以 MySQL 的 InnoDB 为例,官方对 COUNT(*) 做了专门优化,执行时会优先选择一棵较小的辅助索引来遍历,而不是去扫包含所有字段的聚簇索引。辅助索引的叶子节点只存索引列和主键,体积更小,扫描的 IO 成本更低。所以表上存在辅助索引时,COUNT(*)有可能比COUNT(主键列)更快;如果表上没有任何辅助索引,二者差别一般也不大。

我实际测试过一张几百万行的订单表,COUNT(*)和COUNT(1)的执行时间差别可以忽略,执行计划也几乎一致。所以别再纠结选哪个,选一个看着舒服、团队规范认可的就行。我个人习惯统一写COUNT(*),语义直白,谁都能一眼看懂。

但有一个变体要留意:COUNT(可空列)。它需要额外判断每一行该列是否 NULL,虽然现代数据库的 NULL 判断成本不高,但在海量数据上还是比COUNT(*)多一层工作。所以,如果你的目的是统计总行数,不要拿一个“看起来总会有值”的列去顶替 COUNT(),直接写 COUNT() 就好。

1.3 COUNT(DISTINCT 列名):去重计数不是免费的

“这列里一共有多少种不同的值”是另一个高频需求,写法也简单:

SELECT COUNT(DISTINCT region) FROM customers;

这个函数会先对 region 做去重,再统计非 NULL 的值的数量。NULL 在这里依旧不会被计入。举个例子,region 列的值是:华东、华北、NULL、华南、华北。COUNT(DISTINCT region)返回 3——NULL 不进统计,华北重复也只算一次。

听起来很美好,但它的实现代价不小。数据库内部要做一次排序或者哈希去重,数据量大、重复率低的时候,内存和 CPU 消耗都相当可观。所以我一直把 COUNT(DISTINCT) 当成一个“能用但别乱用”的函数。报表页面如果只是展示量级,能少用尽量少用;真要频繁算,不如维护一张预聚合的统计表。

还有一点需要注意:多列去重COUNT(DISTINCT col1, col2)在不同数据库里差异巨大。SQL Server 一直支持这种写法,PostgreSQL 也支持,MySQL 8.0 之后才支持,而 SQLite 不支持。如果不确定当前数据库支持情况,标准做法是先做一次 DISTINCT 子查询,再包一层 COUNT:

SELECT COUNT(*) FROM ( SELECT DISTINCT user_id, device_id FROM user_devices ) t;

这种写法逻辑上所有数据库通用,代价就是要多包一层子查询。干活的时候先确认你用的数据库版本,别写出来之后到了生产环境直接报语法错误。

2. 条件计数:同一个数据源,怎么统计不同状态

统计场景里,很少会一路COUNT(*)到底。更多时候要按状态、按时间、按金额分类计数:已支付的订单多少笔、未支付的多少笔、已取消的多少笔。这就要用到条件计数。

2.1 WHERE + COUNT:先过滤再统计,最简单的条件计数

先看最直白的写法:

SELECT COUNT(*) FROM orders WHERE status = 'PAID';

意思是把 status 为 PAID 的行先筛出来,再数剩下的行数。逻辑清晰,也最好读。它唯一的缺点是:如果同时要统计多个状态,就得写多条 SQL 去查数据库,一次查询只返回一个数字。

业务报表很少只关心一个数字。比如订单列表页上方经常有“全部 / 待付款 / 已付款 / 已取消”四个 Tab,每个 Tab 都要显示对应的订单数。如果按 WHERE + COUNT 的做法,要发四条请求,或者用 UNION ALL 拼起来。虽然也能跑,但明显不够优雅,数据库也被多打了好几次。

2.2 CASE WHEN + COUNT:一次扫描统计多个条件

这时候就该COUNT(CASE WHEN ... THEN 1 END)出场了。它可以在同一条 SQL 里统计多个分类,只扫一遍数据:

SELECT COUNT(*) AS total_orders, COUNT(CASE WHEN status = 'PAID' THEN 1 END) AS paid_orders, COUNT(CASE WHEN status = 'PENDING' THEN 1 END) AS pending_orders, COUNT(CASE WHEN status = 'CANCELED' THEN 1 END) AS canceled_orders FROM orders;

为什么这个写法有效?关键在于数据库里的聚合函数会忽略 NULL,而 CASE WHEN 在不满足条件、且没有 ELSE 子句时,返回的就是 NULL。满足条件的行返回常量 1,计入 COUNT;不满足条件的行返回 NULL,被 COUNT 直接忽略。所以每一列 COUNT 实际上只数了它自己关心的那部分行。

这里有一个新手很容易写错的点:有些人会把 THEN 后面写成 NULL,比如COUNT(CASE WHEN status = 'PAID' THEN NULL END),那结果永远是 0,因为所有行都返回 NULL,全部被忽略。所以 THEN 后面一定要放一个非 NULL 的常量,最省事的就是 1。

如果统计的是数值条件,比如订单金额:

SELECT COUNT(CASE WHEN amount > 100 THEN 1 END) AS high_amount_orders, COUNT(CASE WHEN amount <= 100 THEN 1 END) AS low_amount_orders FROM orders;

注意 CASE 条件要覆盖所有可能,否则就会出现“漏统计”的情况。比如上面的条件如果漏掉 amount 为 NULL 的行,这两列的合计就会比 COUNT(*) 小。NULL 在比较运算中是个特殊的坑:amount > 100遇到 NULL 既不为真也不为假,而是“未知”,CASE 会走到 ELSE。如果不希望遗漏,需要显式处理,比如WHEN amount IS NULL THEN 1 END。

2.3 HAVING + COUNT:分组之后再筛组

条件计数还有一层,是统计完组之后,再按组的统计结果过滤。典型需求:查出订单数超过 10 的客户。

SELECT customer_id, COUNT(*) AS order_cnt FROM orders GROUP BY customer_id HAVING COUNT(*) > 10;

我第一次学 SQL 的时候特别不理解:为什么不能把条件直接写进 WHERE,写成WHERE COUNT(*) > 10?后来才知道这是 SQL 的执行顺序问题。WHERE 是逐行过滤的,发生在分组和聚合之前;而聚合函数要等到 GROUP BY 分组之后才计算出来。等 COUNT(*) 计算出来的时候,WHERE 早就执行完了,你根本没有机会在 WHERE 里用聚合结果。HAVING 就是专门用来干这件事的:它在分组和聚合之后执行,可以引用聚合函数的结果。

一句口诀:WHERE 过滤行,HAVING 过滤组。两者的执行阶段完全不同,别混着用。

3. 分组与去重:报表场景的黄金组合

如果说 CASE WHEN 是条件统计的利器,那 GROUP BY 就是报表统计的地基。几乎所有“按某个维度统计数量”的需求,最后都要落到 GROUP BY + COUNT 上。

3.1 GROUP BY + COUNT 的完整套路

基础写法再复习一遍:

SELECT department_id, COUNT(*) FROM employees GROUP BY department_id;

它把员工表按部门分组,然后数每个部门的人数。GROUP BY 后面跟几个字段,就是按几个字段的组合来分组。比如按日期和渠道统计注册量:

SELECT DATE(register_time) AS reg_date, channel, COUNT(*) FROM users GROUP BY DATE(register_time), channel;

分组统计有一个容易踩的边界:如果分组字段本身有 NULL,这些 NULL 会被单独归为一组。查出来的结果里会出现一个 NULL 分组,如果你在报表里展示,显示出来就是一个“未知”的行。需要排除时,可以在分组前加WHERE channel IS NOT NULL,或者用 COALESCE 把 NULL 转成默认值再分组。

另一个常见问题是 SELECT 里的列必须和 GROUP BY 一致。比如上面这个例子,如果你 SELECT 里多放了一个 user_name,那就麻烦了。在严格模式(比如 MySQL 的 ONLY_FULL_GROUP_BY)下会直接报错;在没有严格模式的数据库里,它可能返回一个“组内随机”的值,在数据上非常危险。所以写分组查询时,记住一条铁律:SELECT 中除了聚合函数外的每一列,都必须出现在 GROUP BY 中。

3.2 多列去重计数:不同数据库的差异不小

在报表里,我经常遇到“我要数不重复的组合”这种需求。比如“统计有多少个不同的用户 + 设备组合在活跃”,如果先按 user_id 单独去重再按 device_id 单独去重,两个结果相加是错的,因为你想要的是组合去重。

不同数据库的支持情况我前面提过一嘴,这里展开说。SQL Server 里可以直接写:

SELECT COUNT(DISTINCT user_id, device_id) FROM active_log;

PostgreSQL 也一样。MySQL 8.0 之后也支持同样的写法。但如果你还在维护 MySQL 5.7 的项目,这条 SQL 会直接报错。需要用子查询曲线救国:

SELECT COUNT(*) FROM ( SELECT DISTINCT user_id, device_id FROM active_log ) t;

这里有个取舍:子查询版本逻辑通用,但把去重结果物化后再计数,内存占用可能更高,尤其是去重组合很多的时候。所以如果是大表高频统计,建议专门建一张“组合维度表”,每天或者每小时把组合聚合好,查询时直接 COUNT。

3.3 统计不同分数段/状态段的组合技巧

分段统计也是报表里的常客。比如一张考试成绩表,想看 90 分以上有多少人、60 到 90 有多少人、不及格有多少人。常规做法是写三条 SQL,但用 CASE WHEN 可以一次搞定:

SELECT COUNT(CASE WHEN score >= 90 THEN 1 END) AS excellent, COUNT(CASE WHEN score >= 60 AND score < 90 THEN 1 END) AS good, COUNT(CASE WHEN score < 60 THEN 1 END) AS failed FROM exam_results;

这种写法我在实际项目中非常喜欢,因为它只查询一次就能把报表页面的好几个数字全部凑齐。尤其是仪表盘场景:同一张表,同一个时间范围,五六个指标全部一条 SQL 返回。不过要注意分段的边界条件,最好用 BETWEEN 或者显式写明大于等于和小于,避免边界值漏统计或者重复统计。

有些朋友可能会用SUM(score >= 60)这种写法,因为 SELECT 里布尔表达式在部分数据库会隐式转成 1/0。但我一般不建议,一方面可读性差,另一方面在 NULL 参与时会得到意外结果。CASE WHEN 是标准 SQL,跨数据库兼容性最好,写起来也不慢。

4. JOIN 里的 COUNT:结果突然翻倍,十有八九是这里的问题

如果说 COUNT 最容易让人怀疑人生的场景,一定是 JOIN。我见过太多次“这张表明明 1000 行,JOIN 之后 COUNT 出来 3500”的求助帖了。

4.1 JOIN 之后 COUNT 翻倍:一对多关系是罪魁祸首

原因其实很简单:JOIN 把两张表做了行的展开。当主表的每一行对上了明细表的多行,主表的那一行就会被复制成多行。比如订单表有 100 条订单,订单明细表里每个订单平均有 3 条商品,两个表按订单 ID JOIN 之后,结果集就变成大约 300 行。这个结果集里每一行都是“一个订单 + 一条明细”的组合。

这时候如果你直接COUNT(*),数出来的当然不是订单数,而是订单明细的组合数量。这不算数据库算错了,而是 SQL 的语义就是这样:COUNT(*) 永远数的是结果集中的实际行数,JOIN 之后的结果集确实被展开成了那么多行。

所以在 JOIN 场景下,第一步要想清楚你到底要数什么:是主表行数?是明细行数?还是有明细才会计数?

4.2 COUNT(DISTINCT 主键) 的价值与代价

最常见的需求是“JOIN 之后还是想要主表的数量”。这时候大家都会想到COUNT(DISTINCT 主表主键):

SELECT COUNT(DISTINCT o.order_id) FROM orders o INNER JOIN order_items i ON o.order_id = i.order_id;

这条 SQL 能正确返回“有关联明细的订单数”。因为 DISTINCT 会把因为 JOIN 而重复出现的 order_id 折叠回一个。

但这个写法是有代价的。主键虽然走索引,但 DISTINCT 去重本身需要额外的排序或哈希操作,表一大,性能就可能成为瓶颈。另一个思路是避免展开主表,先对明细表做 GROUP BY,把明细表压缩成每个订单一行,再 JOIN 主表:

SELECT COUNT(*) FROM orders o INNER JOIN ( SELECT order_id FROM order_items GROUP BY order_id ) t ON o.order_id = t.order_id;

这个方案的思路是:先在明细表内部把“是否有明细”算出来,之后 JOIN 出来的行数和主表一一对应,直接 COUNT(*) 就是正确答案,而且不需要大范围 DISTINCT。实际执行时,明细表的 GROUP BY 仍然需要排序或哈希,但比在 JOIN 结果上做 DISTINCT 通常更可控。

4.3 用 EXISTS 代替 JOIN 计数,大多数时候更省事

如果需求只是“统计至少有一条明细的订单数”,其实根本不值得 JOIN。用 EXISTS 写半连接往往更高效,也更贴合语义:

SELECT COUNT(*) FROM orders o WHERE EXISTS ( SELECT 1 FROM order_items i WHERE i.order_id = o.order_id );

EXISTS 的执行特点是:只要在子查询里找到一条匹配记录就立刻返回,不需要把订单和明细的完整结果集展开,也不会产生重复行。这和“每张订单只算一次”的需求完美匹配。我写过很多类似查询后发现,能用 EXISTS 表达的计数需求,尽量避免显式 JOIN,尤其当主表行很多、明细表匹配度较高时,两者的性能差异非常明显。

但要记住一个前提:如果查出来的报表还需要明细表里的某个字段,比如要展示“订单金额总和”,那就不能只用 EXISTS,还是得 JOIN 明细表和聚合函数配合使用。这时候用上面提到的“先聚合明细表再 JOIN”的方式,比直接 JOIN 后再 COUNT(DISTINCT) 更优雅。

5. 性能优化:COUNT 慢,到底慢在哪

统计金额、订单这类需求往往要跑 COUNT,而 COUNT 一旦变慢,全组都会跟着遭殃。我见过一张三千万行的流水表,执行SELECT COUNT(*) FROM ...能跑 30 多秒。为什么这么慢?

5.1 为什么大表 COUNT 几十秒都跑不完

首先要分清存储引擎。MySQL 的 MyISAM 引擎会在元数据里保存每张表的精确行数,所以它的COUNT(*)不带条件时几乎是秒回。但 MyISAM 因为不支持事务、崩溃恢复差,现在的生产环境基本不用了。InnoDB 是主流,但它并没有为每张表精确缓存行数。

InnoDB 不缓存行数的根本原因是事务隔离。每个事务按隔离级别允许看到不同的数据快照,如果直接在表元数据里存一个“固定行数”,事务 A 和事务 B 看到的结果就会对不上。所以 InnoDB 只能根据当前事务的快照,实时去扫描索引或数据页来统计行数。这意味着无条件 COUNT 一张大表的开销,约等于把表扫一遍。

知道这个原理以后,遇到“COUNT 慢”的求助,我会先排掉 MyISAM 这种过时方案,直接往 InnoDB 的索引扫描和 WHERE 条件下推的方向去查。

5.2 覆盖索引与执行计划:让 COUNT 别再拖慢业务

如果 COUNT 带条件,比如:

SELECT COUNT(*) FROM orders WHERE status = 'PAID';

性能关键看 status 上有没有索引。这里有个细节:InnoDB 的二级索引叶子节点存的是索引列和主键,体积通常比整行小。如果 status 上有索引,数据库大概率会通过扫二级索引的方式来检查 status 并计数,而不是扫整张表的聚簇索引。IO 数据量小一大截,查询自然快。

配合执行计划看得更清楚。执行EXPLAIN SELECT COUNT(*) FROM orders WHERE status = 'PAID';,如果看到 type 为 ref 或 range,Extra 里出现 Using index,说明优化器用上了覆盖索引,路径很健康;如果看到 type 为 ALL,那就是全表扫描,大概率就是慢查询的源头。需要说明的是,如果 status 区分度很差,比如只有两个值且分布均匀,优化器即使走索引也可能扫掉一半的索引页,这时候加索引也只能缓解一部分,可能还是得靠后面的统计表方案。

我自己的习惯是:任何一条涉及 COUNT 的慢 SQL,第一件事先丢到 EXPLAIN 里看扫描行数和访问类型,判断索引用没用上,再决定是加索引、改写法还是上统计表。别一上来就堆缓存和中间件,很多 COUNT 慢只是缺了一个合适的二级索引。

5.3 COUNT 的业务优化清单

如果索引加了还是不满足性能要求,就得换思路。我列几个实际项目中验证过的方案。

第一,维护统计表。专门建一张“订单统计表”,每次插入或更新订单的时候,在同一事务里把计数加一或减一。查询 COUNT 就变成查一张很小的表,几乎瞬间返回。代价是写路径上多了一点开销,但对大量读、少量写的报表系统非常值。

第二,分页总数不一定每次都要精确。经典的场景是列表页右下角要显示“共 30000 条”。如果业务可以接受“超过一定页数后不显示精确总数”,可以用 LIMIT n+1 判断“是否还有更多记录”,比如每页 20 条,就查询 LIMIT 21,返回 21 条说明还有下一页,否则没有。少跑一次大 COUNT,页面响应速度立刻提升。

第三,分区表。如果数据按日期分区,比如WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01',优化器会走分区剪枝,只扫描相关分区的数据,COUNT 的开销也能降很多。

第四,合并 COUNT 查询。前面提到的 CASE WHEN 分组统计,本质就是把多次扫描合并成一次。多指标报表页面,把十几条 COUNT 合并成一条 SQL,减少数据库的往返,这比在代码里循环查库要优雅得多。

6. 常见问题与排查技巧速查

这部分相当于一个实操总结。我平时帮同事排查 SQL 问题,大约 80% 的 COUNT 异常都能在前面的章节里找到原因。这里把排查思路和速查表整理一下。

6.1 排查思路:先从语义入手,再看执行计划

遇到 COUNT 结果不对,我的建议是别急着看代码,先问三个问题:到底要数行还是数非空值?要不要去重?有没有 JOIN 展开?语义一旦明确,90% 的“为什么结果不对”都能定位。

举个例子。之前我排查过一个报表,用户数突然翻了一倍。定位过程是:先确认基础 SQL 是COUNT(*) FROM users,没问题;再看报表配置,发现有 JOIN 到用户行为的明细表。用户行为表每条用户有多行,JOIN 后结果集膨胀,COUNT(*) 自然翻倍。把 SQL 改成COUNT(DISTINCT users.id)之后,数字立刻恢复正常。

如果结果对了但慢,就看执行计划。EXPLAIN 输出里的 type 和 rows 字段是最直接的线索。rows 显示优化器估算的扫描行数,如果估算比实际大很多,可能是统计信息没更新,也可能索引选得不对。配合 EXPLAIN ANALYZE(MySQL 8、PostgreSQL 都有)可以拿到实际执行时间,比光靠猜靠谱得多。

6.2 常见问题速查表

症状可能原因解决方案
COUNT(列名) 比 COUNT(*) 少NULL 被聚合函数忽略明确统计“行数”还是“非空数”,想数行就用 COUNT(*)
JOIN 之后 COUNT 变大一对多关联导致主表行重复COUNT(DISTINCT 主键) 或先聚合明细表
分组结果里多出一个 NULL 组分组字段本身有 NULLWHERE 分组列 IS NOT NULL,或 COALESCE 转默认值
COUNT(CASE WHEN ...) 结果是 0THEN 返回了 NULLTHEN 写 1 等非 NULL 常量
加了 WHERE 后 COUNT 还是慢条件列没有合适索引建二级索引,用 EXPLAIN 验证
COUNT(DISTINCT) 大表很慢去重需要排序或哈希预聚合、统计表,或改用 EXISTS
COUNT(*) 大表无条件也慢InnoDB 需要实时扫描统计表、分区、或者接受估算值

这张表我打印出来贴在工位上过,后来转成团队 Wiki 里的 SQL 评审清单,大家写统计 SQL 之前先对一遍,省了不少事。

6.3 窗口函数 COUNT OVER:统计之外的延伸玩法

COUNT 不只用于分组聚合,放到窗口函数里还能做“运行累计”和“组内占比”。比如要统计“截至每天结束时的累计注册用户数”,通常是先按日期聚合出每日数量,再用窗口函数累加:

SELECT register_date, daily_cnt, SUM(daily_cnt) OVER (ORDER BY register_date) AS running_total FROM ( SELECT register_date, COUNT(*) AS daily_cnt FROM users GROUP BY register_date ) t;

窗口函数COUNT(*) OVER (...)本身也可以对每个分组返回总行数。比如不带 ORDER BY 的写法:

SELECT department_id, emp_name, COUNT(*) OVER (PARTITION BY department_id) AS dept_emp_cnt FROM employees;

它会为每一行返回所在部门的员工数,适合做“部门人数占比”之类的计算。

用窗口函数时有个细节很容易踩:默认窗口的边界定义。MySQL、PostgreSQL、SQL Server 的默认窗口帧大致都是“从分区起点到当前行”,但 RANGE 模式下,如果 ORDER BY 的字段有重复值,相同值的行可能会被一起包含进累计范围。比如 register_date 同一天注册了多个人,运行累计可能一下子加上整天的数量,而不是逐行加。如果业务要求必须逐行精确累计,可以显式声明:

COUNT(*) OVER ( ORDER BY register_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )

ROWS 和 RANGE 的区别,我是在处理用户增长曲线时才真正理解的。当时日活数据按天重复值很多,RANGE 模式导致的累计结果和预期对不上,排查了很久才发现是窗口帧的问题。这里也建议大家写窗口函数时多看一眼边界语义,别默认数据库行为。

我个人这些年做统计需求,最大的体会是:COUNT 本身不难,难的是每次写之前都先想清楚“我到底在数什么”。是数所有行,还是数非空值?是数去重后的主键,还是数带条件的分组?这个 30 秒的语义确认,比任何技巧都值钱。如果顺手养成了看执行计划的习惯,那 COUNT 就再也坑不了你了。希望这篇里分享的坑和优化思路,能让你下次写 COUNT 的时候少开几个浏览器标签页。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询