SQL - 如何识别给定数据中的 1 小时时间段孤岛?

Posted

技术标签:

【中文标题】SQL - 如何识别给定数据中的 1 小时时间段孤岛?【英文标题】:SQL - How to identify 1 hour time period islands in the given data? 【发布时间】:2021-04-08 19:50:12 【问题描述】:

目标是接受收到的第一个投诉,并拒绝在第一个投诉后 1 小时内收到的所有投诉。例如我有下面的数据。

ComplaintID DateTime
1 12/24/2019 1:07 PM
2 12/24/2019 1:20 PM
3 12/24/2019 1:40 PM
4 12/24/2019 2:00 PM
5 12/24/2019 2:10 PM
6 12/24/2019 2:12 PM
7 12/24/2019 2:50 PM
8 12/24/2019 2:55 PM
9 12/24/2019 3:00 PM
10 12/24/2019 3:08 PM
11 12/24/2019 4:00 PM
12 12/24/2019 4:50 PM
13 12/24/2019 7:00 PM
14 12/26/2019 7:01 PM

所需输出:

ComplaintID DateTime Status
1 12/24/2019 1:07 PM Accept
2 12/24/2019 1:20 PM Reject
3 12/24/2019 1:40 PM Reject
4 12/24/2019 2:00 PM Reject
5 12/24/2019 2:10 PM Accept
6 12/24/2019 2:12 PM Reject
7 12/24/2019 2:50 PM Reject
8 12/24/2019 2:55 PM Reject
9 12/24/2019 3:00 PM Reject
10 12/24/2019 3:08 PM Reject
11 12/24/2019 4:00 PM Accept
12 12/24/2019 4:50 PM Reject
13 12/24/2019 7:00 PM Accept
14 12/26/2019 7:01 PM Accept

我知道使用编程语言会很容易,但是我需要 SQL 中的解决方案。

编辑:

根据@Gordon 的建议,我实现了以下递归查询,它可以工作!但是,它在大数据上似乎效率低下。

with RECURSIVE t AS (
    select row_number as rn,ts, lag(ts,1) over (order by row_number) as baseline from main_table where row_number<3
  UNION ALL
    SELECT 
    rn+1 as rn 
    ,(select ts from main_table where row_number=rn+1) as ts
    ,case when datediff('hour',ts,baseline)>24 then ts else baseline end as baseline
     from (select * FROM t order by rn desc limit 1 )t where rn<=(select count(*)-1 from main_table)
)

,real_baseline as 
(
select rn,ts,lead(baseline,1) over (order by rn) as real_baseline from t
)

select * 
,case when row_number() over (partition by real_baseline order by ts) =1 then 'Accept'
else 'Reject' end as status
from real_baseline

【问题讨论】:

我认为这需要递归 CTE —— 这在大量数据上效率不高。 当您说“在 SQL 中”时,您实际上是指纯 SQL,还是仅仅意味着它必须在 DBMS 中发生?它必须是单个查询,还是可以是一个过程?限制的原因是什么? 程序/UDF 也很好。 sql-server 还是 postgres? 查看LAG()LEAD() 分析SQL 函数,并尝试在数据中找到session 的开头以获得结果。 here 或 here 【参考方案1】:

通常您可以应用领先/滞后,但这里不能。领先/滞后的问题是所需的不可预测范围。同样,递归 CTE 似乎不可行,因为它需要递归部分中的 MIN 函数;但是,这是不允许的。由于一个函数是令人满意的,也许最好的函数是返回一个表。见fiddle。

create or replace function public.accept_reject_complaints()
 returns table( o_complaintid integer
              , o_datetime    timestamp 
              , o_status      text
              )
 language plpgsql
AS $$                 
declare
    l_current_end_ts timestamp = '-infinity'::timestamp;

    c_complaint_list cursor for
                     select complaintid, datetime     
                       from complaints
                      order by datetime;
begin
    for complaint_rec in c_complaint_list 
    loop
       if complaint_rec.datetime  > l_current_end_ts then 
          o_status = 'Accept'; 
          l_current_end_ts = complaint_rec.datetime  + interval '1 hour';
       else 
          o_status = 'Reject'; 
       end if; 
   
       o_datetime = complaint_rec.datetime;
       o_complaintid = complaint_rec.complaintid;
       return next; 
    end loop ;

end ; 
$$;

不幸的是,由于它涉及游标循环,因此对于大数据量来说,性能将是一个问题。

【讨论】:

【参考方案2】:

这是简单的方法。将每个日期时间截断为 Hour,然后在每个小时内将 First 或 Minimum datetime 作为接受,其他作为拒绝。

PS 我已经使用 table_name 作为投诉更改它。在 Postgresql 8 中测试。

SELECT ComplaintID,DateTime,CASE WHEN row_number() over(partition by hour order by 
DateTime)=1 THEN 'Accept' else 'Reject' end as Status from 
(select ComplaintID,DateTime ,date_trunc('hour',DateTime)as hour  from complaint)A ;

【讨论】:

我认为你误解了目标。请查看所需的输出以更好地理解它。 根据您的输出图像 2019 年 12 月 24 日下午 1:07 接受然后 2019 年 12 月 24 日下午 3:08 拒绝应该是“接受”,因为在第一次抱怨 2 小时后直到 3 :07 PM 从 3:08 开始是下一个小时。根据我从第一次开始的理解,该小时应该开始,在这种情况下,每个小时都从 2:07 、 3:07 pm 、4:07 pm .. 开始?【参考方案3】:
    利用ComplaintID的连续性,查询为:
with recursive cte as (
  select 1 ComplaintID, min(DateTime) DateTime,
    min(DateTime) prev
    from main_table
  union all
  select t2.ComplaintID, t2.DateTime,
    case when t1.prev + interval '1 hour' < t2.DateTime
         then t2.DateTime else t1.prev end
    from cte t1 join main_table t2
    on t1.ComplaintID+1 = t2.ComplaintID
)
select ComplaintID, DateTime,
  case when DateTime=prev
    then 'Accept' else 'Reject' end Status
  from cte
  order by ComplaintID

DB Fiddle

    提取Accept的每一行,查询为:
with recursive cte as (
  (
    select ComplaintID, DateTime, 'Accept' Status
      from main_table order by DateTime limit 1
  )
  union all
  (
    select t2.ComplaintID, t2.DateTime, 'Accept'
      from cte t1 join main_table t2
      on t1.DateTime + interval '1 hour' < t2.DateTime
      order by t2.DateTime limit 1
  )
)
select t1.ComplaintID, t1.DateTime, coalesce(t2.Status, 'Reject') Status
  from main_table t1 left join cte t2
  on t1.ComplaintID=t2.ComplaintID
  order by t1.ComplaintID

DB Fiddle

【讨论】:

以上是关于SQL - 如何识别给定数据中的 1 小时时间段孤岛?的主要内容,如果未能解决你的问题,请参考以下文章

SQL如何取时间字段的小时和分钟

如何从SQL中的当前时间获取所有时间在下一个小时范围内的数据

每小时持续时间的 SQL 计数事件

SQL Server - 如何根据将插入数据半小时的表中的几个参数来计算持续时间?

识别 Microsoft SQL Server 2005 中未使用的对象

如何在 SQL 或 PL/SQL 中查找给定字符串(列数据)中的第 5 个字符? [关闭]