记录某次工作中摸索出来的骚操作,不确定有无更好的“正统方法”。
数据
假设有以下样式的数据,其中“行号”不是数据内容,只是为了本文索引方便。
表名:content
行号 | date | city | time | status | value |
---|---|---|---|---|---|
1 | 20220621 | 北京 | 08:08:08 | A | 5 |
2 | 20220621 | 北京 | 09:09:09 | A | 5 |
3 | 20220621 | 北京 | 10:10:10 | B | 4 |
4 | 20220621 | 北京 | 11:11:11 | B | 3 |
5 | 20220621 | 北京 | 12:12:12 | B | 3 |
6 | 20220621 | 上海 | 06:12:34 | A | 5 |
7 | 20220621 | 上海 | 17:47:52 | A | 7 |
8 | 20220621 | 上海 | 21:09:33 | B | 2 |
目标是分日期、城市维度,时间升序,统计状态由 A 到 B 且数值降低的记录,分自然小时出现的次数分布。
表中只有第 3、8 行符合这一要求,则分小时的次数统计应该是如下结果:
hour | times |
---|---|
10 | 1 |
21 | 1 |
思路
我想到两种思路实现相邻行的对比。一是建立行号列,用嵌套子查询中的相邻行号做条件,在最内层比较目标字段值,这在《SQL进阶教程》中有类似的介绍。另一种是我这次图快的骚操作,利用窗口函数和字段值的特性来做判断。
1 寻找状态从 A 变更为 B 的行
- SELECT 语句中,用窗口函数选择当前行和上一行。
- 将 first_value() 的值作为当前行的新字段查出来。
在父查询中对比该字段与当前行原字段。
SELECT *
FROM (
SELECT date,
city,
time,
status,
first_value(status) over(PARTITION BY date, city ORDER BY time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS former_status
FROM content
)
WHERE former_status = 'A'
AND status = 'B'
2 寻找数值降低的行
可以使用上文的方法,或者对于数值:
我最早想到的配合窗口函数的常用聚合函数是 avg(),也可以解决这个问题,只不过绕路了。
- 可以用窗口含数求当前行和上一行的平均值,作为新字段。
- 在父查询中对比数值与平均值,若数值小于平均值,则认为当前行数值比上一行数值更小。
SELECT * FROM ( SELECT date, city, time, avg(value) over(PARTITION BY date, city ORDER BY time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS avg_with_former_value FROM content ) WHERE value < avg_with_former_value
最终
SELECT hour(time) as hour,
count(*) as times
FROM (
SELECT date,
city,
time,
status,
value,
first_value(status) over(PARTITION BY date, city ORDER BY time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS former_status,
first_value(value) over(PARTITION BY date, city ORDER BY time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS former_value
FROM content
)
WHERE former_status = 'A'
AND status = 'B'
AND value < former_value
GROUP BY 1
// 原来想这个方法时感觉挺复杂的,怎么写出来觉得这么简单……