如何在Postgres预订中找到第一个免费开始时间

And*_*rus 6 sql postgresql schedule range

除星期日和公众假期外,工作时间为上午10:00至晚上21:00.

他们的工作每隔15分钟保留一次.工作时间为15分钟至4小时.整个工作必须适合单日.

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

例如,Mary已于12:30至16:00预订,John已于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
Run Code Online (Sandbox Code Playgroud)

我尝试了基于如何从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
Run Code Online (Sandbox Code Playgroud)

此外,还不会返回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
Run Code Online (Sandbox Code Playgroud)

架构:

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);
Run Code Online (Sandbox Code Playgroud)

发布到pgsql-general邮件列表.

Erw*_*ter 2

适应模式

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();
Run Code Online (Sandbox Code Playgroud)

主要观点

  • 不要使用char(n). 相反varchar(n),或者更好,varchar或者只是text

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

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

    EXCLUDE USING gist (worker_id WITH =, day WITH =
                      , timerange(work_from, work_to) WITH &&)
    
    Run Code Online (Sandbox Code Playgroud)
  • 由于根据定义,您的范围永远不会跨越日期边界date,因此拥有单独的列(day在我的实现中)和时间范围会更有效。该类型timerange不在默认安装中提供,但可以轻松创建。这样您就可以很大程度上简化您的检查约束。

  • 用于EXTRACT('isodow', ...)简化排除星期日

    一周中的某一天为星期一 (1) 到星期日 (7)

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

  • 假设边界包括下限,不包括上限。

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

除了
简化输入和检查约束之外,我预计与仅占用 4 个字节timerange相比可以节省 8 个字节的存储空间。但事实证明它在磁盘上占用了 22 个字节(RAM 中为 25 个字节),就像(或) 一样。所以你也可以一起去。查询和排除约束的原理是一样的。tsrangetimetimerangetsrangetstzrangetsrange

询问

包装到 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;
Run Code Online (Sandbox Code Playgroud)

称呼:

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

现在在 Postgres 9.3 上使用SQL Fiddle 。

解释

  • 该函数采用 a_start timestamp作为最小启动时间,并且_duration interval。请注意,仅排除开始当天的较早时间,而不是随后的几天。最简单的方法是添加日期和时间:t + d > _start
    要从“现在”开始预订,只需通过now()::timestamp

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

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

有关的:


归档时间:

查看次数:

492 次

最近记录:

9 年,10 月 前