SQL 中如何统计满足条件与不满足条件的数据

陆遥
陆遥
发布时间:2026-08-25 | 85人浏览过
原创

前言

很多 SQL 条件统计的问题,并不是语法写不出来,而是结果在遇到 NULL 后悄悄变了味。本文围绕“满足”和“不满足”两类计数展开,拆解为什么 COUNT 减法不稳、什么情况下必须显式覆盖空值,以及分组统计和跨库复用时该怎样写,读完后你可以直接判断一条统计 SQL 的口径是否可靠。

很多人写条件统计时,会先用 COUNT(CASE WHEN ...) 算出满足条件的数量,再用 COUNT(*) 做减法推导“不满足”的数量。这个写法在字段可能出现 NULL、还要继续做分组比例或跨数据库复用时,往往会把“明确不满足”和“未知值”混在一起。下面就按实际查询场景拆开说明:什么写法更稳,为什么 NULL 必须单独考虑,以及不同数据库里哪些细节最容易出错。

直接用两个 SUM(CASE WHEN) 统计两组数量

如果目标是同时拿到“满足条件”和“不满足条件”的行数,最稳妥的方式就是分别写两个 SUM(CASE WHEN ... THEN 1 ELSE 0 END)。这样每一行都会被明确归类,不会因为聚合函数对 NULL 的处理方式而产生歧义。

典型写法如下:

SELECT
  SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_count,
  SUM(CASE WHEN status != 'paid' OR status IS NULL THEN 1 ELSE 0 END) AS unpaid_or_unknown_count
FROM orders;

这里的关键点有几个:

  • SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) 统计的是明确满足条件的行数。
  • SUM(CASE WHEN status != 'paid' OR status IS NULL THEN 1 ELSE 0 END) 统计的是其余所有行,重点在于把 NULL 显式纳入。
  • 如果字段本身定义了 NOT NULL,第二段可以简化为 SUM(CASE WHEN status != 'paid' THEN 1 ELSE 0 END)。

这类写法的优势不只是结果正确,还在于语义清楚。你在 SQL 里已经把每一类记录的归属写明,后续别人接手查询时,也能直接看懂统计口径。

为什么 COUNT(CASE WHEN) 加减法不可靠

很多人会写成下面这样:

展示 SUM(CASE WHEN) 与 COUNT 减法在 NULL 场景下差异的对比信息图
条件计数里,NULL 为什么会把结果带偏把满足数与不满足数分别显式统计,才能避免把 NULL 混进“不满足”里。
SELECT
  COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid_count,
  COUNT(*) - COUNT(CASE WHEN status = 'paid' THEN 1 END) AS unpaid_count
FROM orders;

表面上这像是在统计“总数减满足数”,但它其实只是在做剩余值推导,并不等于真正的“不满足条件”。问题主要出在 NULL 和三值逻辑上。

NULL 会被混进差值里

假设 status 列中同时存在 'paid'、'pending' 和 NULL:

  • COUNT(CASE WHEN status = 'paid' THEN 1 END) 只统计 paid 行。
  • COUNT(*) 会把全部行都算进去,包括 NULL。
  • 两者相减得到的是“非 paid 的剩余行数”,其中既有明确不满足的记录,也有状态未知的记录。

也就是说,这个差值并不能准确代表业务意义上的“不满足条件”。它只是一个残差集合。

无法区分 false 和 unknown

SQL 不是简单的二值逻辑,而是包含 TRUE、FALSE、UNKNOWN 三种结果。像 status != 'paid' 在遇到 NULL 时,并不会返回 TRUE,而是返回未知。

如果业务要求更细,比如分开统计:

SELECT
  SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_count,
  SUM(CASE WHEN status != 'paid' THEN 1 ELSE 0 END) AS unpaid_count,
  SUM(CASE WHEN status IS NULL THEN 1 ELSE 0 END) AS unknown_status_count
FROM orders;

那么显式分支就不是“可选优化”,而是保证口径准确的必要条件。尤其在报表、对账、审核场景中,把未知值误算成不满足,后面做比例、转化率或状态分布时都会出现偏差。

分组统计时,数量和比例该怎么写

进入 GROUP BY 场景后,通常不只要每组的满足数和不满足数,还会进一步算占比。这里建议把“数量统计”和“比例统计”分开理解。

展示分组统计中数量与比例写法关系的 SQL 信息图
GROUP BY 下如何同时拿到数量和占比分组场景下,数量用 SUM(CASE WHEN),比例可直接用 AVG(CASE WHEN)。

每组数量的稳定写法

SELECT
  category,
  SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS pass_count,
  SUM(CASE WHEN score < 60 OR score IS NULL THEN 1 ELSE 0 END) AS not_pass_count
FROM exams
GROUP BY category;

这种写法的好处是,每个分组里每一行都被明确归类。即便后面要继续联表、嵌套或者做窗口统计,这个基础口径也是稳定的。

每组比例可直接用 AVG(CASE WHEN)

如果还要统计每组及格率,直接写 AVG(CASE WHEN ... THEN 1.0 ELSE 0 END) 会更省事:

SELECT
  category,
  AVG(CASE WHEN score >= 60 THEN 1.0 ELSE 0 END) AS pass_rate
FROM exams
GROUP BY category;
  • AVG(CASE WHEN score >= 60 THEN 1.0 ELSE 0 END) 返回的是小数形式的比例。
  • 它在语义上等价于 SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) * 1.0 / COUNT(*)。
  • 如果要显示百分比,可以再套一层 ROUND(... * 100, 0)。

在工程实践里,这样写通常比手动相除更直接,因为你不用重复展开分子和分母,也更容易看出统计意图。

跨数据库写法的几个关键细节

SUM(CASE WHEN ... THEN 1 ELSE 0 END) 基本属于各主流数据库都支持的通用写法,但迁移时还是有几类细节需要单独检查。

展示跨数据库条件统计细节检查项的信息图
跨库复用时最容易踩的 4 个点通用写法可以迁移,但日期函数、字符串和 NULL 判断仍要逐项检查。

日期函数写法不同

  • MySQL / SQL Server:YEAR(order_date)
  • PostgreSQL:EXTRACT(YEAR FROM order_date)
  • SQLite:strftime('%Y', order_date)

如果你的条件统计里包含按年份筛选、分组或比较,这部分不能直接照抄。

字符串和 NULL 判断要写标准形式

  • 字符串常量要用单引号,比如 'active',不要误写成双引号。
  • 判断空值时要写 status IS NULL,不要依赖 = NULL 或 != NULL。
  • MySQL 如果确实需要做空值安全比较,可以使用 status <=> 'paid'。

这些细节看起来小,但往往就是条件统计结果不对的根源。

多条件并列统计时,一次扫描更划算

如果你要在同一条查询里统计多个条件,例如已支付、待支付、已取消、空状态等,使用多个 SUM(CASE ...) 往往只需要扫描一遍表。相比之下,把每个状态拆成独立子查询或多条 WHERE 聚合,再拼接结果,通常会带来更多扫描成本。

在数据量较大时,这种差异会直接反映在执行时间上。因此,条件分组统计优先集中在一条聚合 SQL 中完成,通常更符合性能和可维护性的平衡。

正式落库前,先检查 NULL 占比

实际项目里最容易被忽略的一步,不是 SQL 能不能跑通,而是你是否确认过数据中到底有没有 NULL。很多统计逻辑在测试库里看起来没问题,只是因为样本数据刚好比较干净。

在正式确定统计口径前,可以先单独查一下空值比例。例如在 PostgreSQL 中:

SELECT COUNT(*) FILTER (WHERE col IS NULL)
FROM your_table;

如果希望直接看比例,也可以用这种通用思路:

SELECT AVG(CASE WHEN col IS NULL THEN 1.0 ELSE 0 END)
FROM your_table;

先看清数据分布,再决定是否把 NULL 纳入“不满足”、单独拆出“未知”,或者在业务层先做清洗,这样后面的统计结果才有解释力。

归根到底,条件统计不是单纯把数字算出来,而是先定义好每一类数据应该落到哪里。只要涉及 NULL、分组比例或跨库迁移,优先使用显式的 SUM(CASE WHEN ... THEN 1 ELSE 0 END),再按需要拆出 IS NULL 分支,通常就是最稳的一种写法。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表