每组内最早出现特定值的记录
问题描述
表 records 记录每个客户每天的下单情况,字段为 row_id(行编号)、customer_id(客户编号)、date(日期)、order_count(当天订单数),每个客户的记录都按 date 递增排列,row_id 与 date 同序。现在要针对每个客户,找出他最早出现 order_count 等于 0 的那条记录的 row_id;从来没有下过零单的客户也要出现在结果里,只是 row_id 给空值。
源数据
row_id |
customer_id |
date |
order_count |
1 |
1 |
2024-01-01 |
2 |
2 |
1 |
2024-01-02 |
1 |
3 |
1 |
2024-01-03 |
5 |
4 |
1 |
2024-01-04 |
0 |
5 |
1 |
2024-01-05 |
3 |
6 |
2 |
2024-01-01 |
5 |
7 |
2 |
2024-01-02 |
3 |
8 |
2 |
2024-01-03 |
9 |
9 |
2 |
2024-01-04 |
1 |
10 |
2 |
2024-01-05 |
3 |
11 |
3 |
2024-01-01 |
0 |
12 |
3 |
2024-01-02 |
1 |
13 |
3 |
2024-01-03 |
5 |
14 |
3 |
2024-01-04 |
0 |
15 |
3 |
2024-01-05 |
3 |
期望结果
每个客户一行,row_id 是他第一个零单日的记录号,没有零单日则为空:
customer_id |
row_id |
1 |
4 |
2 |
null |
3 |
11 |
客户 1 只有一天是零单(2024-01-04,row_id=4),所以结果给 4。
客户 3 有两天是零单(2024-01-01 的 row_id=11、2024-01-04 的 row_id=14),要取更早的那一天,也就是 row_id=11,后一条 14 不能出现在结果里。
客户 2 五天里一天零单都没有,但结果里仍然有他这一行,只是 row_id 是空的。
SQLazy 分步实现
这道题要同时满足两件事:取出零单记录里最早的那一条,又要保住没有零单记录的客户。做法是把这两件事分开:先从原表拿一份完整的客户名单,再从原表按客户分组、只在零单记录里取最小的 row_id,得到 "客户到零单记录" 的对应表,最后把对应表左连接到客户名单上。左连接保证了没配上的客户也留在结果里,配不上的位置自然是空值,正好就是题目要求的 null。
下面逐一解释这些步骤。
Name |
Anchor |
Statement |
t_ids |
records |
derive customer_id |
t_all |
t_ids |
distinct customer_id |
t_zeros |
records |
summarize min row_id condition (order_count=0) as row_id; group customer_id |
t_result |
t_all |
join customer_id; with t_zeros; customer_id; take row_id; left |
第 1 步:取出客户编号这一列
derive customer_id
只留 customer_id 一列,为下一步去重做准备。

第 2 步:得到完整的客户名单
distinct customer_id
按 customer_id 去重,得到每客户一行的完整名单,共 3 行。这份名单用来兜底,保证从未零单的客户也留在结果里。

第 3 步:按客户取最早的零单记录
summarize min row_id condition (order_count=0) as row_id; group customer_id
按 customer_id 分组,聚合算法 min 写在被聚合项 row_id 之前,结果命名为 row_id。条件参数先过滤组内记录,只让 order_count=0 的记录参与聚合,所以算出的是零单记录里最小的 row_id;组内没有零单记录时(如客户 2)结果为空。
取最小 row_id 等于取最早记录,靠的是问题描述里说的 row_id 与 date 同序;数据不同序时要改成按 date 取最小值。
参数次序是聚合、被聚合项、条件、命名,写成先条件后聚合是错的。

第 4 步:把名单和零单记录左连接
join customer_id; with t_zeros; customer_id; take row_id; left
以名单 t_all 为基准表,按 customer_id 关联 t_zeros,拼上它的 row_id。左连接(left)保证名单里的客户一个不丢,配不上的 row_id 为空;用内连接会丢掉客户 2。

编译生成 SQL
确认上述 4 步逻辑后,SQLazy 编译器自动生成原生 SQL(这里是 MySQL 语法):
WITH t_ids AS (
SELECT customer_id
FROM records
),
t_all AS (
SELECT customer_id
FROM t_ids
GROUP BY customer_id
),
t_zeros AS (
SELECT customer_id
, MIN(CASE
WHEN order_count = 0 THEN row_id
ELSE NULL
END) AS row_id
FROM records
GROUP BY customer_id
),
t_result AS (
SELECT t_all.customer_id, t_zeros.row_id
FROM t_all
LEFT JOIN t_zeros ON t_all.customer_id = t_zeros.customer_id
)
SELECT customer_id, row_id
FROM t_result
SQLazy 让你用业务语言描述逻辑,而不是用 SQL 语法写嵌套查询。取最早和保客户这两件事拆成两步各管一件:汇总取零单记录的最小 row_id,左连接保住全部客户。
汇总功能先过滤再聚合:条件参数在聚合前筛一遍,只对零单记录聚合,组内没有零单时自然为空。聚合算法写在被聚合项之前、条件紧随其后,参数次序就是业务说法的次序。
连接方式也是业务语义的直译:保住所有客户写左连接,只留配得上的写内连接。
官方链接
SQLazy 在线体验:sqlazy.com(免费,无需注册)
SQLazy 项目仓库:github.com/SPLWare/SQLazy

英文版 https://c.esproc.com/article/1793677495297