MySQL COUNT(DISTINCT) 忽略 NULL 时如何统计缺失值
来源:17golang原创
时间:2026-09-14 10:58:51 232浏览 收藏
报表要统计“有多少个不同客户”时,COUNT(DISTINCT customer_id) 返回的只是非 NULL 客户数量。它不会把缺失值当成一个客户,也不会把每一行缺失记录都算进去。若业务要把“未填写客户”作为一个独立类别,需要在查询中明确补回一个 NULL 桶。
官方地址:https://dev.mysql.com/doc/refman/8.4/en/
COUNT(DISTINCT customer_id)统计非 NULL 的唯一客户。SUM(customer_id IS NULL)统计缺失记录行数,不是缺失类别数。- 要把 NULL 当成一个类别,可用“非空去重数 + 是否存在 NULL”,或统计
DISTINCT子查询。
先把三个“数”分开:非空去重、缺失行、缺失桶
假设订单表是 orders,其中 customer_id 允许为空。下面这组数据里,客户 101 出现两次,客户 102 出现一次,还有两条订单没有客户编号:
-- 建立一组最小示例,NULL 表示订单没有关联客户
CREATE TEMPORARY TABLE orders (
order_id INT PRIMARY KEY,
store_id INT NOT NULL,
customer_id INT NULL
);
-- 101 重复出现,NULL 出现两次,便于区分三种统计口径
INSERT INTO orders (order_id, store_id, customer_id) VALUES
(1, 10, 101), (2, 10, 101), (3, 10, NULL),
(4, 10, 102), (5, 10, NULL);
此时可以把需求拆成三问:已知客户有几个、缺失订单有几行、把“缺失客户”作为一个类别后共有几类。
| 业务口径 | 写法 | 含义 |
|---|---|---|
| 已知唯一客户数 | COUNT(DISTINCT customer_id) | 忽略 NULL,结果为 2 |
| 缺失订单行数 | SUM(customer_id IS NULL) | 每条 NULL 都计数,结果为 2 |
| 唯一客户类别数 | 已知唯一客户数 + NULL 是否存在 | NULL 只算一个桶,结果为 3 |

用缺失桶公式补回一个 NULL
如果报表要回答“每个门店覆盖了多少种客户状态”,可以把是否存在 NULL 转成 0 或 1,再加到非空去重数上:
-- COUNT(*) 统计行数,COUNT(customer_id) 只统计非 NULL 客户
SELECT
COUNT(DISTINCT customer_id)
+ (COUNT(*) > COUNT(customer_id)) AS customer_bucket_count
FROM orders;
-- 101、102 加上一个 NULL 桶,结果为 3
这里的布尔表达式在 MySQL 中会转成 1 或 0:两者不相等,说明至少有一行 customer_id 为 NULL。空结果集的两个 COUNT 都是 0,表达式不会凭空创建 NULL 桶,这正是通常想要的边界。
不要直接把 NULL 替换成一个看似不会出现的数字或字符串,除非这个哨兵值已经被约束保证永不出现在真实数据里。否则真实客户编号恰好等于哨兵值时,去重结果会少算一个类别;字符串列还可能触发隐式类型转换。
分组统计时,让每个分组独立判断 NULL
把查询按 store_id 分组后,缺失判断必须和聚合一起发生在当前分组内:
-- 每个门店分别计算已知客户、缺失行和 NULL 桶
SELECT
store_id,
COUNT(DISTINCT customer_id) AS known_customer_count,
SUM(customer_id IS NULL) AS missing_order_count,
COUNT(DISTINCT customer_id)
+ (COUNT(*) > COUNT(customer_id)) AS customer_bucket_count
FROM orders
GROUP BY store_id;
-- 不同门店的 NULL 不会被合并,缺失桶只在本门店存在时加 1
COUNT(*) 和 COUNT(customer_id) 都受到同一个 WHERE 条件影响,所以筛选日期、订单状态或门店后再做比较,口径仍然一致。若把全表的 NULL 判断放到分组查询外,就可能给没有缺失值的门店也加上一个桶。

需要稳定通用写法时使用 DISTINCT 子查询
MySQL 的 DISTINCT 会把多个 NULL 视为同一个去重值,因此可以先取唯一值,再统计子查询行数:
-- 子查询保留非 NULL 唯一客户,同时保留一个 NULL 行
SELECT COUNT(*) AS customer_bucket_count
FROM (
SELECT DISTINCT customer_id
FROM orders
) AS unique_customer_values;
-- 子查询的 NULL 只保留一行,结果仍为 3
这种写法更贴近“去重后的值集合有多少行”,适合需要继续查看唯一值集合、或不想维护哨兵值约定的场景。若还要按门店统计,可把 store_id 一并放进子查询的选择列表,再按业务定义决定是统计“门店-客户组合”还是每个门店内部的客户类别。
上线前用口径清单检查查询
- 问题问的是“已知客户数”还是“客户状态类别数”?前者用
COUNT(DISTINCT customer_id),后者才补 NULL 桶。 - 问题问的是缺失记录量,使用
SUM(customer_id IS NULL),不要把它和唯一值数量混用。 - 确认
NULL与空字符串、0、未知客户编号是不同业务状态;COALESCE不会自动替你定义业务含义。 - 分组、过滤和连接后再聚合,确保三个 COUNT 表达式看到的是同一批行。
相关问题
COUNT(column) 和 COUNT(*) 有什么区别?
COUNT(*) 统计筛选后的行数;COUNT(column) 只统计该列非 NULL 的行。判断一组数据是否出现 NULL 时,常比较两者。
NULL 和空字符串会被 COUNT(DISTINCT) 一起忽略吗?
不会。NULL 是缺失值,空字符串是一个实际字符串值;对字符列来说,空字符串可以作为一个非 NULL 的唯一值参与统计。
为什么不总是用 COALESCE(customer_id, -1)?
因为 -1 可能是合法值,且不同列类型需要不同哨兵值。若没有严格约束,优先用“存在 NULL 加一”或 DISTINCT 子查询表达真实口径。
-
374 收藏
-
499 收藏
-
384 收藏
-
184 收藏
-
265 收藏
-
488 收藏
-
440 收藏
-
326 收藏
-
486 收藏
-
329 收藏
-
169 收藏
-
181 收藏
-
338 收藏
-
387 收藏
-
406 收藏
-
195 收藏
-
156 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习