登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

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
MySQL COUNT DISTINCT NULL 统计口径静态关系图,展示 orders 表、customer_id 与三个 COUNT 表达式及 NULL 缺失桶的关系
图1:MySQL 聚合口径的静态关系示意,重点看 customer_id 与三个 COUNT 表达式之间的差别。

用缺失桶公式补回一个 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 判断放到分组查询外,就可能给没有缺失值的门店也加上一个桶。

MySQL 按 store_id 分组统计 COUNT DISTINCT NULL 的查询结构图,展示组内聚合与缺失桶判断关系
图2:按门店分组统计的静态关系示意,缺失判断与去重结果都属于当前 store_id 分组。

需要稳定通用写法时使用 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 子查询表达真实口径。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>