WHERE 用于提前过滤整行以排除特定数据参与分组聚合,如 WHERE status != 'deleted';HAVING 仅能筛选分组结果,无法跳过原始行;需用 CASE WHEN 改写分组键实现逻辑跳过,且所有引用处须同步更新。

WHERE 过滤掉整行,不是“跳过分组”
想让某类数据不参与分组聚合,最直接的方式就是用 WHERE 把它们提前筛掉。比如统计活跃用户订单数,但要把 status = 'deleted' 的订单彻底排除——它们不该进任何组,也不该影响 COUNT 或 SUM。
-
WHERE status != 'deleted'是正确做法;写成HAVING status != 'deleted'会报错或无效果,因为status不在GROUP BY字段里,且HAVING无法访问原始行值 - 注意:如果字段允许 NULL,
WHERE col = ?不会匹配 NULL 行,得显式写WHERE col = ? OR (? IS NULL AND col IS NULL),否则参数为 NULL 时结果为空 - 索引能用上:只要
WHERE条件字段有索引,数据库大概率走索引扫描,性能可控
CASE WHEN 改写分组键,实现“逻辑跳过”
当必须保留这些行(比如要计入总数但不单独成组),就得改造分组依据——把特定值映射成统一占位符,再按这个新表达式分组。
- 例如把
category = 'temp'的行归入'other'类:CASE WHEN category = 'temp' THEN 'other' ELSE category END -
SELECT、GROUP BY、HAVING、ORDER BY中所有出现原字段的地方,都得换成这个CASE表达式,否则语法错或逻辑错 - 原字段上的索引对这个表达式无效;PostgreSQL 可建表达式索引,MySQL 8.0+ 支持函数索引,但得手动加:
CREATE INDEX idx_category_mapped ON t ((CASE WHEN category = 'temp' THEN 'other' ELSE category END));
HAVING 不能跳过原始行,只能砍掉整个分组
HAVING 的作用对象是分组后的聚合结果,不是原始数据行。它没法让某一行“不参与当前组”,只能决定“这个组要不要留下”。
- 常见误用:
HAVING category != 'temp'—— 这句本身语法可能通(取决于数据库),但实际含义是“剔除 category 全为 'temp' 的组”,而不是“把 category = 'temp' 的行从各组中摘出来” - 真正要用
HAVING的场景:比如HAVING COUNT(*) > 5(只留订单数超 5 的用户),或HAVING MAX(created_at) (只留最后操作早于某天的组) - 聚合函数必须出现在
HAVING中,WHERE里写COUNT(*) > 5会直接报错
动态条件为空时跳过过滤,别硬套 OR 1=1
应用层传参可能为空,SQL 里不能写死条件。直接拼 AND field = ? 会导致参数为 NULL 时查不到数据,也不能靠 OR 1=1 防注入——那等于放弃参数绑定,开 SQL 注入口。
- 安全写法用
COALESCE:WHERE field = COALESCE(?, field),? 为 NULL 时等价于field = field,恒真 - 或用
CASE:WHERE field = CASE WHEN ? IS NULL THEN field ELSE ? END - MyBatis 等框架用
<where>+<if>自动处理空条件,生成的 SQL 干净且参数绑定完整
WHERE → GROUP BY → HAVING,中间没有“跳过某行但保留它参与其他组”的魔法操作——要么提前丢掉,要么重定义分组逻辑。最容易被忽略的是:改写分组字段后,所有引用点必须同步更新,漏一个就崩。

















