窗口函数sum使用的一个巧妙场景

161 阅读3分钟

最近遇到了个sql的问题。困扰许久

问题描述

数据库存储了机器的一些信号值。下面是这些信号的一些描述信息

signal_a中0代表机器正常,1代表异常。
time代表时间(单位秒)。
会存在设备断电的情况,断电时间内无信号。

数据样例

signal_atime
11686988491
11686988492
11686988493
01686988494
11686988495
11686988496
11686988499
11686988500
11686988501
11686988502

问题:在时间连续前提下,统计每次异常的持续时间。得到目标数据

start_time(异常开始时间)duration(持续时间)
16869884913
16869884952
16869884994

思考

如果使用java之类的语言,循环遍历这个问题就很容易。但问题要求的是使用sql处理。

展开分析。

1. 时间连续。

这个问题中,只需要考虑a信号值为1的情况。所以先加上where signal_a = 1

然后,需要对前一条后一条数据做判断。可以使用lag、lead函数。这里可以获取后一条数据,后一条数据时间-当前数据时间的值如果是定值1,表示连续。 sql大概是这样的

SELECT 
    signal_a,
    time,
    time - lag(time,1) over(ORDER BY time) dif 
    FROM `test2`
    WHERE signal_a = 1;

这时候可以得到结果

image.png

肉眼可以分辨出,dif不是1即表示存在间断。但交给sql该怎么去分辨。(这里卡了很久,最后请教了大佬)

思考,我们在得到什么样的数据后可以继续分析?

大概是这种

image.png

如果可以根据dif字段得到时间连续的分区,再在每个分区内获取连续的signal_a为1的个数,问题就解决了

2. sum()over() 函数的运用

先放sql

SELECT 
    signal_a,
    time,
    dif,
    sum(dif) over(ORDER BY time) part 
FROM(
    SELECT
        signal_a,
        time,
        time - lag(time,1) over(ORDER BY time)-1  dif
    FROM `test2`
    WHERE signal_a = 1
) tmp;

把上面的查询当做成子查询,并将dif-1,这样窗口函数累加便会得到分区。处理结果如下

image.png

这时候需要对第一条数据进行特殊处理,判断肯定属于第一个分区,将null改为0。(这里使用的if处理的,jym有更好的可以教教我)。代码我省略了qaq

3.获取每个分区的个数

这里的逻辑就比较常规了。使用聚合函数加group by即可。

最终sql

SELECT
    min(time) start_time,
    count(1) duration
FROM (
    SELECT 
        signal_a,
        time,
        dif,
        sum(dif) over(ORDER BY time) part 
    FROM(
        SELECT
            signal_a,
            time,
            IFNULL(time - lag(time,1) over(ORDER BY time)-1  ,0) dif
        FROM `test2`
        WHERE signal_a = 1
    ) tmp
)tem2	
GROUP BY part;

我觉得最巧妙的便是将时间差减1,得到0,再累加

希望能对大家有所帮助。如果有更好的方案,或者这个sql有可以改进的,欢迎留言。

案例中,数据库版本 mysql 8.0.29 ,表名test2