PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment trouver les premières heures de départ gratuites à partir des réservations dans Postgres

Schéma adapté

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

Points majeurs

  • N'utilisez pas char(n) . Plutôt varchar(n) , ou mieux encore, varchar ou juste text .

  • N'utilisez pas le nom d'un worker comme clé primaire. Ce n'est pas nécessairement unique et peut changer. Utilisez plutôt une clé primaire de substitution, de préférence un serial . Effectue également des entrées dans reservat plus petits, index plus petits, requêtes plus rapides, ...

  • Mise à jour : Pour un stockage moins cher (8 octets au lieu de 22) et une manipulation plus simple, j'enregistre le début et la fin en tant que time maintenant et construisez une plage à la volée pour la contrainte d'exclusion :

    EXCLUDE USING gist (worker_id WITH =, day WITH =
                      , timerange(work_from, work_to) WITH &&)
    
  • Étant donné que vos plages ne peuvent jamais franchir la limite de date par définition, il serait plus efficace d'avoir une date distincte colonne (day dans mon implémentation) et une plage de temps . Le type timerange n'est pas livré dans les installations par défaut, mais facile à créer. De cette façon, vous pouvez grandement simplifier vos contraintes de contrôle.

  • Utilisez EXTRACT('isodow', ...) simplifier en excluant les dimanches

  • Je suppose que vous voulez autoriser la bordure supérieure de '21:00'.

  • Les frontières sont supposées inclure pour la limite inférieure et exclure pour la limite supérieure.

  • La vérification si les jours nouveaux / mis à jour se situent dans un délai d'un mois à partir de "maintenant" n'est pas IMMUTABLE . Déplacé de CHECK contrainte au déclencheur - sinon vous pourriez rencontrer des problèmes avec le vidage/restauration ! Détails :

À part
En plus de simplifier les contraintes de saisie et de vérification, je m'attendais à timerange pour économiser 8 octets de stockage par rapport à tsrange depuis time n'occupe que 4 octets. Mais il s'avère que timerange occupe 22 octets sur le disque (25 en RAM), tout comme tsrange (ou tstzrange ). Vous pouvez donc utiliser tsrange aussi bien. Le principe de la requête et de la contrainte d'exclusion sont les mêmes.

Requête

Enveloppé dans une fonction SQL pour une gestion pratique des paramètres :

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;

Appel :

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

SQL Fiddle sur Postgres 9.3 maintenant.

Expliquez

  • La fonction prend un _start timestamp comme heure de début minimale et _duration interval . Veillez à n'exclure que les heures antérieures sur le début jour, pas les jours suivants. Le plus simple en ajoutant simplement le jour et l'heure :t + d > _start .
    Pour effectuer une réservation commençant "maintenant", il suffit de passer now()::timestamp :

    SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
    
  • Sous-requête d génère des jours à partir de la valeur d'entrée _day . Jours fériés exclus.

  • Les jours sont croisés avec les plages de temps possibles générées dans la sous-requête t .
  • Qui est joint à tous les nœuds de calcul disponibles w .
  • Enfin, éliminez tous les candidats qui entrent en conflit avec les réservations existantes à l'aide d'un NOT EXISTS l'anti-semi-jointure, et en particulier l'opérateur de recouvrement && .

Connexe :