PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Führen Sie diese Betriebsstundenabfrage in PostgreSQL durch

Tabellenlayout

Gestalten Sie die Tabelle neu, um die Öffnungszeiten (Betriebsstunden) als Satz von tsrange zu speichern (Bereich von timestamp without time zone ) Werte. Erfordert Postgres 9.2 oder höher .

Wählen Sie eine zufällige Woche aus, um Ihre Öffnungszeiten festzulegen. Ich mag die Woche:
1996-01-01 (Montag) bis 1996-01-07 (Sonntag)
Das ist das letzte Schaltjahr, in dem der 1. Januar praktischerweise ein Montag ist. In diesem Fall kann es sich jedoch um eine beliebige Woche handeln. Sei einfach konsequent.

Installieren Sie das Zusatzmodul btree_gist zuerst:

CREATE EXTENSION btree_gist;

Siehe:

  • Äquivalent zur Ausschlussbeschränkung, bestehend aus Ganzzahl und Bereich

Erstellen Sie dann die Tabelle wie folgt:

CREATE TABLE hoo (
   hoo_id  serial PRIMARY KEY
 , shop_id int NOT NULL -- REFERENCES shop(shop_id)     -- reference to shop
 , hours   tsrange NOT NULL
 , CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
 , CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
 , CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
);

Der Eine Spalte hours ersetzt alle Ihre Spalten:

opens_on, closes_on, opens_at, closes_at

Beispielsweise Öffnungszeiten ab Mittwoch, 18:30 bis Donnerstag, 05:00 UTC werden eingegeben als:

'[1996-01-03 18:30, 1996-01-04 05:00]'

Die Ausschlussbeschränkung hoo_no_overlap verhindert überlappende Einträge pro Shop. Es wird mit einem GiST-Index implementiert , was auch unsere Abfragen unterstützt. Beachten Sie das Kapitel "Index und Leistung" unten Erörterung von Indizierungsstrategien.

Die Check-Einschränkung hoo_bounds_inclusive erzwingt inklusive Grenzen für Ihre Bereiche, mit zwei bemerkenswerten Konsequenzen:

  • Ein Zeitpunkt, der genau auf die untere oder obere Grenze fällt, ist immer enthalten.
  • Benachbarte Einträge für denselben Shop sind effektiv unzulässig. Bei inklusiven Grenzen würden sich diese "überschneiden" und die Ausschlussbeschränkung würde eine Ausnahme auslösen. Angrenzende Einträge müssen stattdessen zu einer einzigen Zeile zusammengeführt werden. Außer wenn sie gegen Sonntag Mitternacht enden , in diesem Fall müssen sie in zwei Zeilen aufgeteilt werden. Die Funktion f_hoo_hours() unten kümmert sich darum.

Die Check-Einschränkung hoo_standard_week erzwingt die äußeren Grenzen der Staging-Woche mit dem Operator „Bereich ist enthalten in“ <@ .

Mit inklusive Grenzen, müssen Sie einen Eckfall beachten wo die Zeit um Sonntag Mitternacht umläuft:

'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
 Mon 00:00 = Sun 24:00 (= next Mon 00:00)

Sie müssen nach beiden Zeitstempeln gleichzeitig suchen. Hier ist ein verwandter Fall mit exklusiv Obergrenze, die diesen Mangel nicht aufweisen würde:

  • Benachbarte/überlappende Einträge mit EXCLUDE in PostgreSQL verhindern

Funktion f_hoo_time(timestamptz)

Um jeden gegebenen timestamp with time zone zu "normalisieren". :

CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
  RETURNS timestamp
  LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT timestamp '1996-01-01' + ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
$func$;

PARALLEL SAFE nur für Postgres 9.6 oder höher.

Die Funktion benötigt timestamptz und gibt timestamp zurück . Es addiert das verstrichene Intervall der jeweiligen Woche ($1 - date_trunc('week', $1) in UTC-Zeit zum Startpunkt unserer Staging-Woche. (date + interval erzeugt timestamp .)

Funktion f_hoo_hours(timestamptz, timestamptz)

Um Bereiche zu normalisieren und diejenigen aufzuteilen, die Mo 00:00 überschreiten. Diese Funktion nimmt ein beliebiges Intervall (als zwei timestamptz ) und erzeugt ein oder zwei normalisierte tsrange Werte. Es deckt alle ab legale Eingabe und verbietet den Rest:

CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
  RETURNS TABLE (hoo_hours tsrange)
  LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE COST 500 ROWS 1 AS
$func$
DECLARE
   ts_from timestamp := f_hoo_time(_from);
   ts_to   timestamp := f_hoo_time(_to);
BEGIN
   -- sanity checks (optional)
   IF _to <= _from THEN
      RAISE EXCEPTION '%', '_to must be later than _from!';
   ELSIF _to > _from + interval '1 week' THEN
      RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
   END IF;

   IF ts_from > ts_to THEN  -- split range at Mon 00:00
      RETURN QUERY
      VALUES (tsrange('1996-01-01', ts_to  , '[]'))
           , (tsrange(ts_from, '1996-01-08', '[]'));
   ELSE                     -- simple case: range in standard week
      hoo_hours := tsrange(ts_from, ts_to, '[]');
      RETURN NEXT;
   END IF;

   RETURN;
END
$func$;

Zum INSERT eine einzelne Eingabezeile:

INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');

Für alle Anzahl der Eingabezeilen:

INSERT INTO hoo(shop_id, hours)
SELECT id, f_hoo_hours(f, t)
FROM  (
   VALUES (7, timestamptz '2016-01-11 00:00+0', timestamptz '2016-01-11 08:00+0')
        , (8, '2016-01-11 00:00+1', '2016-01-11 08:00+1')
   ) t(id, f, t);

Jeder kann zwei Zeilen einfügen, wenn ein Bereich um Mo 00:00 UTC geteilt werden muss.

Abfrage

Mit dem angepassten Design Ihre ganze große, komplexe, teure Abfrage kann durch ... ersetzt werden:

SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());

Für ein wenig Spannung habe ich eine Spoilerplatte über die Lösung gelegt. Bewegen Sie die Maus über es.

Die Abfrage wird durch den GiST-Index unterstützt und ist schnell, selbst für große Tabellen.

db<>hier fummeln (mit weiteren Beispielen)
Altes sqlfiddle

Wenn Sie die Gesamtöffnungszeiten (pro Geschäft) berechnen möchten, finden Sie hier ein Rezept:

  • Arbeitszeit zwischen 2 Daten in PostgreSQL berechnen

Index und Leistung

Der Containment-Operator für Bereichstypen kann mit einem GiST oder SP-GiST unterstützt werden Index. Beide können verwendet werden, um eine Ausschlussbeschränkung zu implementieren, aber nur GiST unterstützt mehrspaltige Indizes:

Derzeit unterstützen nur die Indextypen B-Tree, GiST, GIN und BRIN mehrspaltige Indizes.

Und die Reihenfolge der Indexspalten ist wichtig:

Ein GiST-Index mit mehreren Spalten kann mit Abfragebedingungen verwendet werden, die eine beliebige Teilmenge der Spalten des Index umfassen. Bedingungen für zusätzliche Spalten schränken die vom Index zurückgegebenen Einträge ein, aber die Bedingung für die erste Spalte ist die wichtigste, um zu bestimmen, wie viel vom Index gescannt werden muss. Ein GiST-Index ist relativ wirkungslos, wenn seine erste Spalte nur wenige unterschiedliche Werte enthält, selbst wenn es viele unterschiedliche Werte in zusätzlichen Spalten gibt.

Wir haben also widersprüchliche Interessen hier. Für große Tabellen gibt es viel mehr eindeutige Werte für shop_id als für hours .

  • Ein GiST-Index mit führender shop_id ist schneller zu schreiben und die Ausschlussbeschränkung durchzusetzen.
  • Aber wir suchen hours in unserer Abfrage. Es wäre besser, diese Spalte zuerst zu haben.
  • Wenn wir nach shop_id suchen müssen Bei anderen Abfragen ist ein einfacher Btree-Index dafür viel schneller.
  • Zur Krönung habe ich noch ein SP-GiST gefunden Index auf nur hours am schnellsten zu sein für die Abfrage.

Benchmark

Neuer Test mit Postgres 12 auf einem alten Laptop. Mein Skript zum Generieren von Dummy-Daten:

INSERT INTO hoo(shop_id, hours)
SELECT id
     , f_hoo_hours(((date '1996-01-01' + d) + interval  '4h' + interval '15 min' * trunc(32 * random()))            AT TIME ZONE 'UTC'
                 , ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC')
FROM   generate_series(1, 30000) id
JOIN   generate_series(0, 6) d ON random() > .33;

Ergibt ~ 141.000 zufällig generierte Zeilen, ~ 30.000 eindeutige shop_id , ~ 12.000 verschiedene hours . Tabellengröße 8 MB.

Ich habe die Ausschlussbeschränkung gelöscht und neu erstellt:

ALTER TABLE hoo
  DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (shop_id WITH =, hours WITH &&);  -- 3.5 sec; index 8 MB
    
ALTER TABLE hoo
  DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (hours WITH &&, shop_id WITH =);  -- 13.6 sec; index 12 MB

shop_id first ist für diese Distribution ~ 4x schneller.

Außerdem habe ich zwei weitere auf Leseleistung getestet:

CREATE INDEX hoo_hours_gist_idx   on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours);  -- !!

Nach VACUUM FULL ANALYZE hoo; , habe ich zwei Abfragen ausgeführt:

  • Q1 :spät in der Nacht, nur 35 Zeilen finden
  • Q2 :am Nachmittag, Finden von 4547 Zeilen .

Ergebnisse

Sie haben einen Nur-Index-Scan erhalten für jeden (außer natürlich "kein Index"):

index                 idx size  Q1        Q2
------------------------------------------------
no index                        38.5 ms   38.5 ms 
gist (shop_id, hours)    8MB    17.5 ms   18.4 ms
gist (hours, shop_id)   12MB     0.6 ms    3.4 ms
gist (hours)            11MB     0.3 ms    3.1 ms
spgist (hours)           9MB     0.7 ms    1.8 ms  -- !
  • SP-GiST und GiST sind gleichauf bei Abfragen, die wenige Ergebnisse liefern (GiST ist sogar noch schneller für sehr wenige).
  • SP-GiST skaliert besser mit einer wachsenden Anzahl von Ergebnissen und ist auch kleiner.

Wenn Sie viel mehr lesen als schreiben (typischer Anwendungsfall), behalten Sie die eingangs vorgeschlagene Ausschlussbeschränkung bei und erstellen Sie einen zusätzlichen SP-GiST-Index, um die Leseleistung zu optimieren.