每组内最早出现特定值的记录

问题描述

表 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 一列,为下一步去重做准备。

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

Picture8png
第 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 取最小值。
参数次序是聚合、被聚合项、条件、命名,写成先条件后聚合是错的。

Picture10png
第 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。

Picture11png

编译生成 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