使用休息时间生成系列

generate series using break time

我有一个 table 存储营业时间和结束时间

    CREATE TABLE public.open_hours
(
  id bigint NOT NULL,
  open_hour character varying(255),
  end_hour character varying(255),
  day character varying(255),
  CONSTRAINT pk_open_hour_id PRIMARY KEY (id)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE public.open_hours
  OWNER TO postgres;

我还有一个 table 那个

CREATE TABLE public.break_hours
(
id bigint ,
start_time character varying(255),
end_time character varying(255),
open_hour_id bigint ,
CONSTRAINT break_hours_pkey PRIMARY KEY (id),
 CONSTRAINT fkinhl5x01pnn54nv15ol5ntxr5 FOREIGN KEY (open_hour_id )
  REFERENCES public.open_hours(id) MATCH SIMPLE
  ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
OIDS=FALSE
);
ALTER TABLE public.break_hours
OWNER TO postgres;

我需要根据休息时间生成 30 分钟间隔的时间序列。

例如:如果我的开放时间是 08:00 上午,结束时间是 06:00 下午,我的休息时间是 11:00 上午到 11:30 和另一个休息时间是 03:00 PM 到 03:15 PM 然后我需要生成从 08:00 AM 到 11:00 AM 和 11:30 AM 到 03:00 PM 和 03:15 到 06:00 下午。

示例数据

open_hours
-----------
id              open_hours                  end_hour    day
 1              08:00 AM                    06:00 PM    Monday

break_hours


id        start_time   end_time   open_hour_id
 1        11:00 AM     11:30 AM    1
 2        03:00 PM     03:15 PM    1

Sample out put
--------------
08:00 AM
08:30 AM
09:00 AM
09:30 AM
10:00 AM
10:30 AM
11:30 AM
12:00 PM
12:30 PM
01:00 PM
01:30 PM
02:PM PM
02:30 PM
03:15 PM
03:45 PM
04:15 PM
04:45 PM
05:15 PM

Query used for generating series between open hours is
SELECT DISTINCT gs AS start_time,gs + interval '30min' as end_time 
                     FROM   generate_series( timestamp '2018-11-09 08:00 AM', timestamp '2018-11-09 06:00 PM', interval '30min' )gs 
                     ORDER BY start_time

看来你的table造型要清理干净了。例如。您不应将时间存储为文本类型,而应存储为 time without time zone


demo: db<>fiddle

WITH hours AS (
    SELECT 
        oh.open_hour + '1970-01-01'::date as open_hour, 
        oh.end_hour + '1970-01-01'::date as end_hour, 
        bh.start_time + '1970-01-01'::date as break_start,
        bh.end_time + '1970-01-01'::date as break_end,
        lead(start_time + '1970-01-01'::date) OVER (ORDER BY start_time) as next_start_time
    FROM open_hours oh
    LEFT JOIN break_hours bh
    ON oh.id = bh.start_date
)
SELECT generate_series(open_hour, break_start, interval '30 minutes')::time as time_slot 
FROM (
    SELECT 
        open_hour, break_start
    FROM hours
    ORDER BY break_start
    LIMIT 1
)s

UNION 

SELECT 
    generate_series(break_end, next_start_time, interval '30 minutes')::time
FROM ( 
    SELECT 
        break_end, next_start_time
    FROM
        hours
    WHERE next_start_time IS NOT NULL
) s

UNION

SELECT generate_series(break_end, end_hour, interval '30 minutes')::time 
FROM (
    SELECT 
        break_end, end_hour
    FROM hours
    ORDER BY break_start DESC
    LIMIT 1
) s

说明:

WITH 子句 (CTE):

合并两个 table。我添加了一个无意义的日期,因为这会导致 timestamp。后面使用的函数 generate_series 仅适用于 timestamps 而不适用于类型 time。该部分在 ::time 生成之后被切掉。

CTE 的结果是:

open_hour             end_hour              break_start           break_end             next_start_time
1970-01-01 08:00:00   1970-01-01 18:00:00   1970-01-01 09:30:00   1970-01-01 09:45:00   1970-01-01 11:00:00
1970-01-01 08:00:00   1970-01-01 18:00:00   1970-01-01 11:00:00   1970-01-01 11:30:00   1970-01-01 15:00:00
1970-01-01 08:00:00   1970-01-01 18:00:00   1970-01-01 15:00:00   1970-01-01 15:15:00   (NULL)

UNION部分:

这部分包含三个子部分。因为我必须合并来自 tables:

的时间序列

1. 取开放时间。生成一个时间序列到第一次休息开始。

为此,我只需要上面 CTE 的第一行。这就是使用 LIMIT 1 的原因。

2. For all breaks: 生成从当前break结束到下一个break开始的时间序列。

CTE 包含一个 window function lead(),它将下一行的 start_time 移动到当前行(查看 CTE 结果的最后一列)。所以现在我可以得到所有的休息时间,不管有多少。在我的示例中,我添加了从 9:309:45 的第三个中断来演示它。因此可以从所有这些列(当前 break_endnext_start_time)生成下一个时间序列。只有最后一行不包含 next_start_time 因为有 none.

3. 最后一步:生成从最后一个休息时间到收盘时间的时间序列。

这与 (1) 类似。在迭代所有休息时间后,我必须将最后一个时间序列从最后一个休息时间添加到关闭时间。这可以通过过滤没有 next_start_time 的行或排序 DESC 并像我一样使用 LIMIT 1 来实现。


具有更多天类型的更复杂的案例:

demo: db<>fiddle

WITH hours AS (
    SELECT 
        oh.id as day_id,
        oh.open_hour + '1970-01-01'::date as open_hour, 
        oh.end_hour + '1970-01-01'::date as end_hour, 
        bh.start_time + '1970-01-01'::date as break_start,
        bh.end_time + '1970-01-01'::date as break_end,
        lead(start_time + '1970-01-01'::date) OVER (PARTITION BY oh.id ORDER BY start_time) as next_start_time
    FROM open_hours oh
    LEFT JOIN break_hours bh
    ON oh.id = bh.start_date
)

SELECT day_id, generate_series(open_hour, break_start, interval '30 minutes')::time as time_slot 
FROM (
    SELECT DISTINCT ON (day_id)
        day_id, open_hour, break_start
    FROM hours
    ORDER BY day_id, break_start
)s

UNION 

SELECT 
    day_id, generate_series(break_end, next_start_time, interval '30 minutes')::time
FROM ( 
    SELECT  
        day_id, break_end, next_start_time
    FROM
        hours
    WHERE next_start_time IS NOT NULL
) s

UNION

SELECT day_id, generate_series(break_end, end_hour, interval '30 minutes')::time 
FROM (
    SELECT DISTINCT ON (day_id)
        day_id, break_end, end_hour
    FROM hours
    ORDER BY day_id, break_start DESC
) s

ORDER BY day_id, time_slot

主要思想与示例中的相同仅一天。不同之处在于我们必须考虑不同的日期类型。我扩展了上面的示例并添加了具有不同开放时间和休息时间的第二天。

变化:

  1. CTE 中的 window 函数有一个 PARTITION BY 部分。这确保只有 start_times 被转移到同一天。
  2. LIMIT 1 将不再有效,因为它将整个 table 限制为一行。这已更改为 DISTINCT ON (day_id),它将 table 限制为每天的第一行。