Postgres 函数确定一个区间内的周末数

Posted

技术标签:

【中文标题】Postgres 函数确定一个区间内的周末数【英文标题】:Postgres function to determine number of weekends in an interval 【发布时间】:2013-08-27 04:19:42 【问题描述】:

我正在尝试更新一个存储过程,该过程确定从收到票证起的响应时间。在表中,我有收到票时的时间戳(ref_dttm TIMESTAMP WITHOUT TIME ZONE)和第一次响应票时的时间戳(first_action_dttm TIMESTAMP WITHOUT TIME ZONE)。在计算响应时间时,我需要考虑营业时间、周末和假期休息时间。

目前该函数计算间隔并可以减去他们的营业时间,但我似乎无法找到排除周末和节假日的方法。基本上我需要每周减少 15 小时(0900-1800 开放)和每个周末和节假日的 24 小时。

根据收到票的星期几和时间跨度:

Select 
    extract(dow from ref_dttm) as dow, 
    extract(days from (ref_dttm - first_action_dttm) as days

有没有一种简单的方法可以确定已经过了多少个周末?

这是我目前所拥有的 - 它每天减去 15 小时,不包括周末:

CREATE TEMP TABLE tmp_ticket_delta ON COMMIT DROP AS
  SELECT id,ticket_id,ticket_num
    ,(ticket_dttm - first_action_dttm) as delta
    ,extract(days from (ticket_dttm - first_action_dttm)) as days
    ,ticket_descr
  FROM t_tickets
  WHERE ticket_action_by > 0

SELECT id,ticket_id,ticket_num,delta,days,ticket_descr,
  CASE WHEN days = 0 THEN
    CASE WHEN extract(hour from delta) > 15 THEN
      --less than one day but outside of business hours so subtract 15 hrs
      delta - INTERVAL '15:00:00.000'
    ELSE
      delta
    END
  ELSE
    CASE WHEN extract(hour from delta) > 15 THEN
      --take the total number of hours - closing hours + delta - closed hours
      (((days * 24) - (days * 15)) * '1 hour'::INTERVAL) + delta - INTERVAL '15:00:00.000' - (days * '1 day'::INTERVAL)
    ELSE
      (((days * 24) - (days * 15)) * '1 hour'::INTERVAL) + delta - (days * '1 day'::INTERVAL)
    END 
  END AS adj_diff
FROM tmp_ticket_delta

【问题讨论】:

每周一到周五计算 9 小时不是更容易吗? 【参考方案1】:

我喜欢将重要的业务数据存储在表格中。像这样的查询

select min(cal_date), 
       max(cal_date), 
       sum(hours_open) total_time_open, 
       sum(hours_closed) total_time_closed
from daily_hours_open_and_closed
where cal_date between '2013-08-28' and '2013-09-03';

当它们基于存储在简单表中的数据时,易于理解、维护和调试。

我会从calendar table 开始,然后为您的营业地点添加一张桌子。此表“open_times”是最简单的开始方式,但对您的业务而言可能过于简单。例如,您可能需要更严格的 CHECK 约束。此外,我没有尝试提高效率,尽管最终查询在我的开发盒上只运行了 12 毫秒。

create table open_times (
  bus_open timestamp primary key,
  bus_closed timestamp not null
    check (bus_closed > bus_open)
);

用 2013 年的工作日时间填充该表的快速而肮脏的方法。

with openings as (
  select generate_series(timestamp '2013-01-01 09:00', 
                         timestamp '2013-12-31 18:00', '1 day') bus_open
)
insert into open_times
select bus_open, bus_open + interval '9 hours' bus_closed
from openings
where extract(dow from bus_open) between 1 and 5
order by bus_open;

劳动节在这里是假期,所以 9 月 2 日星期一是假期。删除 2013-09-02。

delete from open_times
where bus_open = '2013-09-02 09:00';

这是我唯一感兴趣的假期,目的是展示它是如何运作的。当然,你必须比我做得更好。

为了让事情更简单,创建一个以间隔显示每日运行时间的视图。

create view daily_hours_open_and_closed as
select c.cal_date, 
       ot.bus_open,
       ot.bus_closed,
       coalesce(bus_closed - bus_open, interval '0 hours') as hours_open,
       interval '24 hours' - (coalesce(bus_closed - bus_open, interval '0 hours')) as hours_closed
from calendar c
left join open_times as ot 
       on c.cal_date = cast(ot.bus_open as date);

现在,我们在 2013 年 8 月 28 日和 2013 年 9 月 3 日之间的 7 天里开放了多少小时,关闭了多少小时?原始数据的查询现在非常简单。

select *
from daily_hours_open_and_closed
where cal_date between '2013-08-28' and '2013-09-03'
order by cal_date;

cal_date     bus_open              bus_closed            hours_open  hours_closed
--
2013-08-28   2013-08-28 09:00:00   2013-08-28 18:00:00   09:00:00    15:00:00
2013-08-29   2013-08-29 09:00:00   2013-08-29 18:00:00   09:00:00    15:00:00
2013-08-30   2013-08-30 09:00:00   2013-08-30 18:00:00   09:00:00    15:00:00
2013-08-31                                               00:00:00    24:00:00
2013-09-01                                               00:00:00    24:00:00
2013-09-02                                               00:00:00    24:00:00
2013-09-03   2013-09-03 09:00:00   2013-09-03 18:00:00   09:00:00    15:00:00

使用聚合函数进行算术运算。

select min(cal_date), 
       max(cal_date), 
       sum(hours_open) total_time_open, 
       sum(hours_closed) total_time_closed
from daily_hours_open_and_closed
where cal_date between '2013-08-28' and '2013-09-03'

min          max          total_time_open   total_time_closed
--
2013-08-28   2013-09-03   36:00:00           132:00:00

【讨论】:

【参考方案2】:

您可以使用generate_series计算区间内的周六和周日数量:

-- sample data
with t as (
   (select 1 as id, '2012-01-01'::timestamp as tstart, '2012-02-01'::timestamp as tend) union all -- 9
   (select 2 as id, '2011-12-31'::timestamp as tstart, '2012-02-04'::timestamp as tend) union all -- 11
   (select 3 as id, '2011-12-30'::timestamp as tstart, '2012-02-05'::timestamp as tend) union all -- 12
   (select 4 as id, '2011-12-30'::timestamp as tstart, '2012-02-07'::timestamp as tend)           -- 12
)

-- Calculate number of weekend days

select
   id, 
   sum((dow not between 1 and 5)::int) number_of_weekend_days
from
   (select id, extract(dow from generate_series(tstart,tend,'1 day')) as dow from t) x
group by
   id

如果你有很多数据,我认为这会很慢。

【讨论】:

【参考方案3】:

您可以通过以下查询来计算周末:

select
    *,
    (extract(week from ref_dttm) - extract(week from first_action_dttm)) * 2 -
    case extract(dow from first_action_dttm) when 0 then 1 else 0 end +
    case extract(dow from ref_dttm) when 0 then 2 when 6 then 1 else 0 end
from t_tickets

试试sql fiddle demo

或者如果你的日期可能有不同的年份:

select
    *,
    trunc(date_part('day', ref_dttm - first_action_dttm) / 7) * 2 + 
    case extract(dow from first_action_dttm) when 0 then 1 when 6 then 2 else 0 end + 
    case extract(dow from ref_dttm) when 6 then 1 when 0 then 2 else 0 end -
    case when extract(dow from ref_dttm) = extract(dow from first_action_dttm) then 2 else 0 end as weekends
from t_tickets

试试sql fiddle demo

【讨论】:

以上是关于Postgres 函数确定一个区间内的周末数的主要内容,如果未能解决你的问题,请参考以下文章

HDU3555 区间的数里面有49的个数(数位dp)

9.9 另一个区间问题

给定时间内的 Postgres SUM

BZOJ 4262 线段树+期望

pandas使用bdate_range函数获取起始时间(start)和结束时间(end)范围内的所有周末日期(weekends day)

序列操作