正确做法是用 left join 将分组表作主表,把时间条件写在 on 子句中,再用 where 右表关联字段 is null 筛出零购买分组;若将时间条件误放 where,left join 会退化为 inner join,漏掉无订单分组。

用 LEFT JOIN + WHERE IS NULL 筛出零购买分组
直接结论:不能只靠 WHERE order_date 过滤,那样会漏掉“压根没订单”的分组。正确做法是把分组表(比如 <code>customers 或 categories)作为主表,左连接过去一年的订单,再筛出右表为 NULL 的记录。
常见错误是写成 SELECT * FROM groups g JOIN orders o ON g.id = o.group_id WHERE o.order_date —— 这只会返回有订单且时间在一年内的分组,完全排除了零订单分组。
- 必须用
LEFT JOIN,不能用INNER JOIN或JOIN - 订单过滤条件必须写在
ON子句里,不是WHERE;否则LEFT JOIN会被退化成INNER JOIN - 假设分组主表叫
groups,订单表叫orders,关联字段为group_id
ON 条件里写时间范围,不是 WHERE
这是最容易踩的坑。如果把时间条件放在 WHERE,SQL 会先完成 LEFT JOIN,再过滤结果——此时所有没订单的行在 orders 字段上都是 NULL,而 NULL 是 <code>UNKNOWN,整行被剔除。
正确写法示例:
SELECT g.id, g.name FROM groups g LEFT JOIN orders o ON g.id = o.group_id AND o.order_date >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR) WHERE o.group_id IS NULL;
注意:AND o.order_date >= ... 在 ON 里,不是 WHERE;WHERE o.group_id IS NULL 才是判断“该分组在过去一年内无任何订单”的关键。
考虑订单表可能没有索引导致慢查询
如果 orders 表很大,但没在 (group_id, order_date) 上建联合索引,这个 LEFT JOIN 会非常慢——MySQL 得对每条 groups 记录,全表扫描 orders 去匹配。
- 执行
EXPLAIN看是否用了type: ref或range,避免type: ALL - 推荐索引:
CREATE INDEX idx_group_date ON orders (group_id, order_date); - 如果
order_date允许NULL,确保索引能覆盖非空值(多数引擎没问题,但旧版本 MySQL 有例外)
GROUP BY + COUNT 比较难写对,不推荐
有人想用 GROUP BY + HAVING COUNT(CASE WHEN ... THEN 1 END) = 0,逻辑上可行但容易出错:
-
COUNT(NULL)返回 0,但COUNT(*)永远 ≥ 1(即使没匹配行,LEFT JOIN也会保留左表一行),所以得用COUNT(o.group_id) - 仍需把时间条件放在
ON,否则COUNT统计的是全部历史订单 - 性能通常不如
IS NULL方案,尤其当零订单分组占比高时
除非业务要求同时统计“有订单数”和“零订单分组”,否则别绕弯子。
真正麻烦的是“过去一年”定义模糊——比如要不要包含今天?是否按自然年?这些边界要跟业务对齐,DATE_SUB(CURDATE(), INTERVAL 1 YEAR) 是从当前时刻倒推 365 天,不是固定起止日。











