sql >> Database teknologi >  >> RDS >> PostgreSQL

Sådan finder du de første gratis starttider fra reservationer i Postgres

Tilpasset skema

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();

Vigtige punkter

  • Brug ikke char(n) . Snarere varchar(n) , eller endnu bedre, varchar eller bare text .

  • Brug ikke navnet på en arbejder som primær nøgle. Det er ikke nødvendigvis unikt og kan ændre sig. Brug en surrogat primær nøgle i stedet, bedst en serial . Indtaster også i reservat mindre, indekser mindre, forespørgsler hurtigere, ...

  • Opdatering: For billigere lagring (8 bytes i stedet for 22) og enklere håndtering gemmer jeg start og slut som time nu og konstruer et interval i farten for ekskluderingsbegrænsningen:

    EXCLUDE USING gist (worker_id WITH =, day WITH =
                      , timerange(work_from, work_to) WITH &&)
    
  • Da dine intervaller aldrig kan krydse datogrænsen per definition ville det være mere effektivt at have en separat date kolonne (day i min implementering) og et tidsinterval . Typen timerange leveres ikke i standardinstallationer, men oprettes nemt. På denne måde kan du stort set forenkle dine check-begrænsninger.

  • Brug EXTRACT('isodow', ...) for at forenkle ekskludering af søndage

  • Jeg går ud fra, at du vil tillade den øvre kant af '21:00'.

  • Grænser antages at være inkluderet for den nedre og ekskluderende for den øvre grænse.

  • Kontrollen om nye/opdaterede dage ligger inden for en måned fra "nu" er ikke IMMUTABLE . Flyttede den fra CHECK begrænsning af udløseren - ellers kan du løbe ind i problemer med dump/gendannelse! Detaljer:

Bortset
Udover at forenkle input- og kontrolbegrænsninger forventede jeg timerange for at spare 8 bytes lager i forhold til tsrange siden time kun optager 4 bytes. Men det viser sig timerange optager 22 bytes på disken (25 i RAM), ligesom tsrange (eller tstzrange ). Så du kan gå med tsrange såvel. Princippet om forespørgsel og udelukkelse er det samme.

Forespørgsel

Pakket ind i en SQL-funktion for praktisk parameterhåndtering:

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;

Ring til:

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

SQL Fiddle på Postgres 9.3 nu.

Forklar

  • Funktionen tager en _start timestamp som minimum starttid og _duration interval . Vær omhyggelig med kun at udelukke tidligere tider ved start dag, ikke de følgende dage. Det enkleste ved blot at tilføje dag og klokkeslæt:t + d > _start .
    For at reservere en reservation fra "nu" skal du blot indtaste now()::timestamp :

    SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
    
  • Underforespørgsel d genererer dage startende fra inputværdien _day . Helligdage undtaget.

  • Dage er krydsforbundet med mulige tidsintervaller genereret i underforespørgsel t .
  • Den er krydsforbundet med alle tilgængelige arbejdere w .
  • Eliminér endelig alle kandidater, der kolliderer med eksisterende reservationer ved hjælp af en NOT EXISTS anti-semi-join, og især overlapningsoperatoren && .

Relateret:



  1. Google Apps Script til eksport af regneark til mySQL udføres på flere filer

  2. Sådan udvides kommasepareret felt til flere rækker i MySQL

  3. Ufanget undtagelse 'PDOException'-meddelelse 'ugyldigt datakildenavn'

  4. Hvordan forbinder jeg mySQL i mit Android-projekt?