Sql 如何从Postgres的预订中找到第一次免费开始时间

Sql 如何从Postgres的预订中找到第一次免费开始时间,sql,postgresql,range,schedule,Sql,Postgresql,Range,Schedule,除星期日和公共假日外,人们的工作时间为上午10:00至晚上21:00 每15分钟为他们保留一次作业。作业持续时间为15分钟至4小时。整个工作必须适合一天 如何在Postgres 9.3中找到从当前日期和时间开始的第一个最近的自由开始时间,这些时间不是为给定的持续时间保留的 例如,玛丽已经在12:30预订了房间。。16:00及 约翰已经预订了12:00到13:00的房间 Reservat表包含保留,yksus2表包含工作和 pyha表包含公共假日。下表为结构。如果这有帮助,可以更改Reservat

除星期日和公共假日外,人们的工作时间为上午10:00至晚上21:00

每15分钟为他们保留一次作业。作业持续时间为15分钟至4小时。整个工作必须适合一天

如何在Postgres 9.3中找到从当前日期和时间开始的第一个最近的自由开始时间,这些时间不是为给定的持续时间保留的

例如,玛丽已经在12:30预订了房间。。16:00及 约翰已经预订了12:00到13:00的房间

Reservat表包含保留,yksus2表包含工作和 pyha表包含公共假日。下表为结构。如果这有帮助,可以更改Reservat结构

应返回查询持续时间为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
我尝试根据下面的答案进行查询,但返回了错误的结果:

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);
另外。

适应模式 因为根据定义,您的范围永远不会跨越日期边界,所以在我的实现中使用单独的日期列day和时间范围会更有效。这样可以大大简化检查约束

星期一至星期日

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

假设边界包括下限,排除上限

检查新的/更新的天数是否在一个月之内不是不变的。将其从检查约束移动到触发器-否则您可能会遇到转储/还原问题!详情:

旁白 除了简化输入和检查约束外,我还希望timerange比tsrange节省8字节的存储空间,因为时间只占用4字节。但事实证明,timerange在RAM的磁盘25上占用了22个字节,就像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;
电话:

现在是9.3级

解释 该函数采用_开始时间戳作为最小开始时间和_持续时间间隔。注意只排除开始当天的较早时间,而不是接下来的几天。只需添加日期和时间即可实现最简单的操作:t+d>\u start。 要从现在开始预订,只需传递now::timestamp:

子查询d从输入值_day开始生成天。节假日除外

天数与子查询t中生成的可能时间范围交叉连接。 交叉连接到所有可用工人w。 最后,使用NOT EXISTS反半联接消除与现有保留冲突的所有候选项,特别是重叠运算符&&。 相关的:

例如日期数学
psql通用邮件列表中的Thom Brown推荐以下解决方案

它更具可读性,但欧文的答案看起来更优化。 我有10个工作和1个月的预约,从8点到20点有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;

添加了@Andrus:我检查了我的浏览器历史记录。SQLFIDLE链接应该是正确的。恐怕小提琴在这里滑落了。性能:10*31*36=11160个候选插槽不是什么,但绝对可以管理。我的一些观点是强有力的建议。剩下的是需求和品味的问题。我会选择最好的答案和测试性能。我添加和d.d>current\u date或t.t>current\u time来查询where子句以消除过去的时间。这样可以吗?上:小提琴:可能是撞车什么的。。。pg9.3为超负荷。在第9.2页添加了一个新的提琴。上:参见我的注释:我假设您希望允许“21:00”的上边框。消除过去的时间:不,你的表达不起作用。请参阅更新的答案。注意:我将函数名缩短为f_next_free。21:00是工作日的结束时间。作业此时无法启动。不包括21:00。所以我不明白安德鲁斯是怎么做的;根据需要调整返回的列。厄普费里
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);
SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
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;