sql >> データベース >  >> RDS >> PostgreSQL

Postgresの予約から最初の無料開始時間を見つける方法

    適応スキーマ

    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にもエントリを作成します 小さい、インデックスが小さい、クエリが速い、...

    • 更新: より安価なストレージ(22ではなく8バイト)とより簡単な処理のために、開始と終了をtimeとして保存します。 ここで、除外制約の範囲をその場で作成します。

      EXCLUDE USING gist (worker_id WITH =, day WITH =
                        , timerange(work_from, work_to) WITH &&)
      
    • 範囲は日付の境界を越えることはできませんので 定義上、個別のdateを使用する方が効率的です。 列(day 私の実装では)および時間範囲タイプtimerange デフォルトのインストールでは出荷されませんが、簡単に作成できます。 このようにして、チェック制約を大幅に簡素化できます。

    • EXTRACT('isodow', ...) 日曜日を除く単純化する

    • 許可したいと思います 「21:00」の上枠。

    • 境界線は、下限を含み、上限を除外すると想定されます。

    • 新しい/更新された日が「今」から1か月以内にあるかどうかのチェックは、IMMUTABLEではありません。 。 CHECKから移動しました トリガーへの制約-そうしないと、ダンプ/復元で問題が発生する可能性があります!詳細:


    入力とチェック制約を単純化することに加えて、私はtimerangeを期待していました tsrangeと比較して8バイトのストレージを節約します time以降 4バイトしか占有しません。しかし、timerangeであることが判明しました tsrangeと同じように、ディスク上で22バイト(RAM内で25バイト)を占有します。 (または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フィドル Postgres9.3で今すぐ。

    説明

    • この関数は_startを取ります timestamp 最小開始時間および_duration intervalとして 。 開始の早い時間のみを除外するように注意してください 翌日ではなく、日。日時を追加するだけで最も簡単:t + d > _start
      「今」から始まる予約を予約するには、now()::timestampを渡すだけです。 :

      SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
      
    • サブクエリd 入力値_dayから始まる日数を生成します 。休日は除きます。

    • 日は、サブクエリtで生成される可能な時間範囲と相互結合されます 。
    • これは、利用可能なすべてのワーカーに相互結合されますw
    • 最後に、NOT EXISTSを使用して、既存の予約と衝突するすべての候補を削除します 反半結合、特にオーバーラップ演算子&&

    関連:



    1. &apos;の交換方法またはXMLELEMENTOracleを使用する場合の特殊文字

    2. MySQLエラーコード:1030ストレージエンジンからエラー-1が発生しました。データベースからデータを削除しようとしました

    3. IMAPとPHP-送信済みフォルダと受信トレイフォルダからすべてのメールを取得する

    4. MySQLで自動インクリメントがリセットされない