【问题标题】:How to find first free start times from reservations in Postgres如何从 Postgres 的预订中找到第一个免费开始时间
【发布时间】:2015-10-17 09:15:00
【问题描述】:

人们从上午 10:00 到晚上 21:00 工作,周日和公众假期除外。

每隔 15 分钟为他们保留一次工作。工作时间从 15 分钟到 4 小时不等。整个工作必须适合一天。

如何从当前日期和时间开始在 Postgres 9.3 中查找未保留给定持续时间的第一个最近的免费开始时间?

例如,玛丽已经预订了 12:30 .. 16:00 和 约翰已经预订了 12:00 到 13:00

Reservat 表包含预订,yksus2 表包含作品和 pyha 表包含公共假期。表结构如下。如果有帮助,可以更改预留结构。

查询 1.5 小时的最开始时间应该返回

John 2014-10-28 10:00
Mary 2014-10-28 10:00
John 2014-10-28 10:15
Mary 2014-10-28 10:15
John 2014-10-28 10:30
Mary 2014-10-28 10:30
Mary 2014-10-28 11:00
John 2014-10-28 13:00
Mary 2014-10-28 16:00
Mary 2014-10-28 16:15
Mary 2014-10-28 16:30
... etc and also starting from next days

我尝试根据下面How to return only work time from reservations in PostgreSql? 中的答案进行查询,但它返回错误的结果:

MARY  2014-10-28 13:00:00
MARY  2014-10-29 22:34:40.850255
JOHN  2014-10-30 22:34:40.850255
MARY  2014-10-31 22:34:40.850255
MARY  2014-11-03 22:34:40.850255

也不会返回滑动开始时间 10:00、10:30 等。
如何获得正确的首次预订?

返回错误结果的查询是:

insert into reservat (objekt2, during) values 
('MARY', '[2014-10-28 11:30:00,2014-10-28 13:00:00)'), 
('JOHN', '[2014-10-28 10:00:00,2014-10-28 11:30:00)');

with gaps as (
    select
        yksus, 
        upper(during) as start,
        lead(lower(during),1,upper(during)) over (ORDER BY during) - upper(during) as gap
    from (
        select 
           yksus2.yksus,
           during
          from reservat join yksus2 on reservat.objekt2=yksus2.yksus 
          where  upper(during)>= current_date
        union all
        select
            yksus2.yksus,
            unnest(case
                when pyha is not null then array[tsrange1(d, d + interval '1 day')]
                when date_part('dow', d) in (0, 6) then array[tsrange1(d, d + interval '1 day')]
                when d::date =  current_Date then array[
                            tsrange1(d, current_timestamp ), 
                            tsrange1(d + interval '20 hours', d + interval '1 day')]
                else array[tsrange1(d, d + interval '8 hours'), 
                           tsrange1(d + interval '20 hours', d + interval '1 day')]
            end)
        from yksus2, generate_series(
            current_timestamp,
            current_timestamp + interval '1 month',
            interval '1 day'
        ) as s(d) 
        left join pyha on pyha = d::date
    ) as x 
)

select yksus, start
  from gaps 
where gap >= interval'1hour 30 minutes'
order by start
limit 30

架构:

CREATE EXTENSION btree_gist;
CREATE TABLE Reservat (
      id serial primary key,
      objekt2 char(10) not null references yksus2 on update cascade deferrable,
      during tsrange not null check(
         lower(during)::date = upper(during)::date
         and lower(during) between current_date and current_date+ interval'1 month'

         and (lower(during)::time >= '10:00'::time and upper(during)::time < '21:00'::time) 
         AND EXTRACT(MINUTE FROM lower(during)) IN (0, 15, 30,45)
         AND EXTRACT(MINUTE FROM upper(during)) IN (0, 15, 30, 45)
         and (date_part('dow', lower(during)) in (1,2,3,4,5,6) 
         and date_part('dow', upper(during)) in (1,2,3,4,5,6)) 
      ),

      EXCLUDE USING gist (objekt2 WITH =, during WITH &&)
    );  

create or replace function holiday_check() returns trigger language plpgsql stable as $$
    begin
        if exists (select * from pyha  where pyha in (lower(NEW.during)::date, upper(NEW.during)::date)) then
            raise exception 'public holiday %', lower(NEW.during) ;
        else
            return NEW;
        end if;
    end;
    $$;

create trigger holiday_check_i before insert or update on Reservat for each row execute procedure holiday_check();

CREATE OR REPLACE FUNCTION public.tsrange1(start timestamp with time zone,
    finish timestamp with time zone ) RETURNS tsrange AS
$BODY$
SELECT tsrange(start::timestamp without time zone, finish::timestamp without time zone );
$BODY$ language sql immutable;


-- Workers
create table yksus2( yksus char(10) primary key);
insert into yksus2 values ('JOHN'), ('MARY');

-- public holidays
create table pyha( pyha date primary key);

还有posted to the pgsql-general mailing list

【问题讨论】:

标签: sql postgresql range schedule


【解决方案1】:

适应的架构

CREATE EXTENSION btree_gist;
CREATE TYPE timerange AS RANGE (subtype = time);  -- create type once

-- Workers
CREATE TABLE worker(
   worker_id serial PRIMARY KEY
 , worker text NOT NULL
);
INSERT INTO worker(worker) VALUES ('JOHN'), ('MARY');

-- Holidays
CREATE TABLE pyha(pyha date PRIMARY KEY);

-- Reservations
CREATE TABLE reservat (
   reservat_id serial PRIMARY KEY
 , worker_id   int NOT NULL REFERENCES worker ON UPDATE CASCADE
 , day         date NOT NULL CHECK (EXTRACT('isodow' FROM day) < 7)
 , work_from   time NOT NULL -- including lower bound
 , work_to     time NOT NULL -- excluding upper bound
 , CHECK (work_from >= '10:00' AND work_to <= '21:00'
      AND work_to - work_from BETWEEN interval '15 min' AND interval '4 h'
      AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
      AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
    )
 , EXCLUDE USING gist (worker_id WITH =, day WITH =
                     , timerange(work_from, work_to) WITH &&)
);
INSERT INTO reservat (worker_id, day, work_from, work_to) VALUES 
   (1, '2014-10-28', '10:00', '11:30')  -- JOHN
 , (2, '2014-10-28', '11:30', '13:00'); -- MARY

-- Trigger for volatile checks
CREATE OR REPLACE FUNCTION holiday_check()
  RETURNS trigger AS
$func$
BEGIN
   IF EXISTS (SELECT 1 FROM pyha WHERE pyha = NEW.day) THEN
      RAISE EXCEPTION 'public holiday: %', NEW.day;
   ELSIF NEW.day < now()::date OR NEW.day > now()::date + 31 THEN
      RAISE EXCEPTION 'day out of range: %', NEW.day;
   END IF;

   RETURN NEW;
END
$func$ LANGUAGE plpgsql STABLE; -- can be "STABLE"

CREATE TRIGGER insupbef_holiday_check
BEFORE INSERT OR UPDATE ON reservat
FOR EACH ROW EXECUTE PROCEDURE holiday_check();

要点

  • 不要使用 char(n)。而是varchar(n),或者更好的是varchar,或者只是text

  • 不要使用工作人员的名字作为主键。它不一定是唯一的,可以改变。请改用代理主键,最好使用serial。还使reservat 中的条目更小、索引更小、查询更快……

  • 更新:为了更便宜的存储(8 个字节而不是 22 个字节)和更简单的处理,我现在将 start 和 end 保存为 time,并为排除约束动态构建一个范围:

    EXCLUDE USING gist (worker_id WITH =, day WITH =
                      , timerange(work_from, work_to) WITH &&)
    
  • 由于根据定义,您的范围永远不会跨越日期边界,因此使用单独的 date 列(在我的实现中为 day)和 时间范围The type timerange is not shipped in default installations, but easily created. 这样可以大大简化检查约束。

  • Use EXTRACT('isodow', ...) to simplify excluding sundays

    星期一(1) 到星期日(7) 是星期几

  • 我假设您希望允许“21:00”的上边界。

  • 假设边界包含下限而排除上限。

  • 检查新/更新日期是否在“现在”一个月内不是IMMUTABLE。将其从CHECK 约束移至触发器 - 否则您可能会遇到转储/恢复问题!详情:

旁白
除了简化输入和检查约束之外,我希望 timerangetsrange 相比节省 8 个字节的存储空间,因为 time 只占用 4 个字节。但事实证明timerange 占用磁盘上的 22 个字节(RAM 中的 25 个字节),就像tsrange(或tstzrange)一样。所以你也可以选择tsrange。查询和排除约束的原理是一样的。

查询

包装成一个SQL函数方便参数处理:

CREATE OR REPLACE FUNCTION f_next_free(_start timestamp, _duration interval)
  RETURNS TABLE (worker_id int, worker text, day date
               , start_time time, end_time time) AS
$func$
   SELECT w.worker_id, w.worker
        , d.d AS day
        , t.t AS start_time
        ,(t.t + _duration) AS end_time
   FROM  (
      SELECT _start::date + i AS d
      FROM   generate_series(0, 31) i
      LEFT   JOIN pyha p ON p.pyha = _start::date + i
      WHERE  p.pyha IS NULL   -- eliminate holidays
      ) d
   CROSS  JOIN (
      SELECT t::time
      FROM   generate_series (timestamp '2000-1-1 10:00'
                            , timestamp '2000-1-1 21:00' - _duration
                            , interval '15 min') t
      ) t  -- times
   CROSS  JOIN worker w
   WHERE  d.d + t.t > _start  -- rule out past timestamps
   AND    NOT EXISTS (
      SELECT 1
      FROM   reservat r
      WHERE  r.worker_id = w.worker_id
      AND    r.day = d.d
      AND    timerange(r.work_from, r.work_to) && timerange(t.t, t.t + _duration)
      )
   ORDER  BY d.d, t.t, w.worker, w.worker_id
   LIMIT  30  -- could also be parameterized
$func$ LANGUAGE sql STABLE;

呼叫:

SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);

SQL Fiddle 现在在 Postgres 9.3 上。

解释

  • 该函数将_start timestamp 作为最小开始时间,_duration interval。请注意仅排除开始日的较早时间,而不是接下来的几天。最简单的方法是添加日期和时间:t + d &gt; _start
    要从“现在”开始预订,只需通过now()::timestamp

    SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
    
  • 子查询 d 从输入值 _day 开始生成天数。节假日除外。

  • 天与子查询t中生成的可能时间范围交叉连接。
  • 这是交叉加入到所有可用的工作人员w
  • 最后使用NOT EXISTS 反半连接消除所有与现有预留冲突的候选,尤其是重叠运算符&amp;&amp;

相关:

【讨论】:

  • @Andrus:我检查了我的浏览器历史记录。 sqlfiddle 链接应该是正确的。恐怕 sqlfiddle 在这里滑倒了。性能:10 * 31 * 36 = 11160 个候选插槽不是没有,但绝对是可管理的。我的一些观点是强烈的建议。剩下的就是需求和品味的问题了。我会选择最好的答案和 test 性能。
  • 我添加了 and (d.d&gt;current_date or t.t&gt;current_time) 来查询 where 子句以消除过去的时间。这样可以吗?
  • upper:fiddle:可能是崩溃之类的……pg9.3 负担过重。在 pg9.2 上添加了一个新的小提琴。 upper:见我的笔记:I assume you want to allow the upper border of '21:00'. 消除过去的时间:不,你的表达方式不行。请参阅更新的答案。注意:我将函数名称缩短为f_next_free()
  • 21:00 是工作日结束。此时无法启动作业。 NOT upper_inc(during) 不包括 21:00。所以我不明白'
  • @Andrus;根据您的需要调整返回的列。 upper(during) &lt;= '21:00' 表示预订必须在 21:00 之前或之前结束NOT upper_inc(during) 强制排除上限。应该是这样的。
【解决方案2】:

psql-general 邮件列表中的 Thom Brown 推荐以下解决方案。

它更具可读性,但 Erwin 的答案看起来更优化。 我有 10 件作品和 1 个月的预订,从 8 点到 20:00 有 15 分钟的休息时间,所以性能完全不是问题。 用哪个?

哪种解决方案更好?

create table pyha (pyha date primary key);
insert into pyha(pyha) values('2014-10-29');
create table  yksus2(yksus char(10) primary key);
insert into yksus2 values ('JOHN'),('MARY');
CREATE EXTENSION btree_gist;
CREATE TABLE reservat
(
reservat_id serial primary key,
      objekt2 char(10) not null references yksus2 on update cascade deferrable,
during tstzrange not null,

EXCLUDE USING gist (objekt2 WITH =, during WITH &&),

CONSTRAINT same_date
     CHECK (lower(during)::date = upper(during)::date),

CONSTRAINT max_1month_future 
     CHECK (lower(during) between current_date and current_date+ interval'1 month' ),

CONSTRAINT time_between_1000_and_2100
     CHECK (lower(during)::time >= '10:00'::time and upper(during)::time < '21:00'::time),

CONSTRAINT lower_bound_included
     CHECK (lower_inc(during)),

CONSTRAINT upper_bound_excluded
     CHECK (not upper_inc(during)),

CONSTRAINT start_time_at_15minute_offset
     CHECK (EXTRACT(MINUTE FROM lower(during)) IN (0, 15, 30,45)),
-- or (extract(epoch from lower(during)::time)::int % (60*15) = 0)

CONSTRAINT end_time_at_15minute_offset
     CHECK (EXTRACT(MINUTE FROM upper(during)) IN (0, 15, 30,45)),

CONSTRAINT duration_between_15min_and_4hours
     CHECK (upper(during) - lower(during) between '15 mins'::interval and '4 hours'::interval),

CONSTRAINT exclude_sundays
     CHECK (date_part('dow', lower(during)) in (1,2,3,4,5,6) )
);

create or replace function holiday_check() returns trigger language plpgsql stable as $$
    begin
        if exists (select * from pyha  where pyha between lower(NEW.during)::date and upper(NEW.during)::date) then
            raise exception 'public holiday %', lower(NEW.during) ;
        else
            return NEW;
        end if;
    end;
    $$;

create trigger holiday_check_i before insert or update on Reservat for each row execute procedure holiday_check();
INSERT INTO reservat (objekt2, during)
  VALUES ('MARY','[2014-10-29 11:30+2,2014-10-29 13:00+2)'::tstzrange);
INSERT INTO reservat (objekt2, during)
  VALUES ('JOHN','[2014-10-29 10:00+2,2014-10-29 11:30+2)'::tstzrange);


   SELECT yksus2.yksus, times.period
FROM generate_series(now()::date::timestamptz, now()::date::timestamptz + '3 months'::interval, '15 mins'::interval) times(period)
CROSS JOIN yksus2
LEFT JOIN reservat ON tstzrange(times.period,times.period + '1 hour 30 mins'::interval, '[)') && reservat.during
  AND yksus2.yksus = reservat.objekt2
LEFT JOIN pyha ON times.period::date = pyha.pyha::date
WHERE reservat.during IS NULL
  AND pyha.pyha IS NULL
  AND times.period::timetz BETWEEN '10:00'::timetz AND '21:00'::timetz - '1 hour 30 mins'::interval
  AND times.period >= now()
  AND EXTRACT(isoDOW FROM times.period) != 7 -- exclude sundays
ORDER BY 2, 1
LIMIT 300;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-06-04
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 2021-11-30
    • 2021-10-15
    • 1970-01-01
    • 2018-09-19
    相关资源
    最近更新 更多