sql >> Database >  >> RDS >> PostgreSQL

Voer deze bedrijfsurenquery uit in PostgreSQL

Tabelindeling

Herontwerp de tabel om openingstijden (openingsuren) op te slaan als een set van tsrange (bereik van timestamp without time zone ) waarden. Vereist Postgres 9.2 of hoger .

Kies een willekeurige week om je openingstijden in te delen. Ik hou van de week:
1996-01-01 (maandag) tot 1996/01/07 (zondag)
Dat is het meest recente schrikkeljaar waarin 1 januari toevallig een maandag is. Maar in dit geval kan het elke willekeurige week zijn. Wees gewoon consistent.

Installeer de extra module btree_gist eerst:

CREATE EXTENSION btree_gist;

Zie:

  • Equivalent aan uitsluitingsbeperking bestaande uit geheel getal en bereik

Maak dan de tabel als volgt aan:

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]')
);

De ene kolom hours vervangt al uw kolommen:

opens_on, closes_on, opens_at, closes_at

Bijvoorbeeld openingstijden vanaf woensdag 18:30 tot donderdag, 05:00 UTC worden ingevoerd als:

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

De uitsluitingsbeperking hoo_no_overlap voorkomt overlappende boekingen per winkel. Het is geïmplementeerd met een GiST-index , die ook onze vragen ondersteunt. Bekijk het hoofdstuk "Index en prestaties" hieronder bespreken we indexeringsstrategieën.

De controlebeperking hoo_bounds_inclusive handhaaft inclusieve grenzen voor uw bereiken, met twee opmerkelijke gevolgen:

  • Een tijdstip dat precies op de onder- of bovengrens valt, is altijd inbegrepen.
  • Aangrenzende inzendingen voor dezelfde winkel zijn in feite niet toegestaan. Met inclusieve grenzen zouden die "overlappen" en zou de uitsluitingsbeperking een uitzondering veroorzaken. Aangrenzende items moeten in plaats daarvan worden samengevoegd tot één rij. Behalve wanneer ze rond zondag middernacht rondlopen , in welk geval ze in twee rijen moeten worden gesplitst. De functie f_hoo_hours() hieronder zorgt hiervoor.

De controlebeperking hoo_standard_week dwingt de buitengrenzen van de staging-week af met behulp van de "range is bevat door"-operator <@ .

Met inclusief grenzen, moet u een hoekgeval . in acht nemen waar de tijd rond zondag middernacht loopt:

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

U moet beide tijdstempels tegelijk zoeken. Hier is een verwante zaak met exclusief bovengrens die deze tekortkoming niet zou vertonen:

  • Aangrenzende/overlappende items voorkomen met EXCLUDE in PostgreSQL

Functie f_hoo_time(timestamptz)

Om een ​​gegeven timestamp with time zone te "normaliseren" :

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 alleen voor Postgres 9.6 of hoger.

De functie duurt timestamptz en retourneert timestamp . Het voegt het verstreken interval van de respectievelijke week toe ($1 - date_trunc('week', $1) in UTC-tijd naar het startpunt van onze stageweek. (date + interval produceert timestamp .)

Functie f_hoo_hours(timestamptz, timestamptz)

Om de bereiken te normaliseren en de overschrijdingen van ma 00:00 te splitsen. Deze functie heeft elk interval (als twee timestamptz ) en produceert een of twee genormaliseerde tsrange waarden. Het dekt elke juridische invoer en verbiedt de 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$;

Naar INSERT een enkele invoerrij:

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

Voor elke aantal invoerrijen:

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

Elk kan twee rijen invoegen als een bereik moet worden gesplitst om ma 00:00 UTC.

Zoekopdracht

Met het aangepaste ontwerp je hele grote, complexe, dure vraag kan worden vervangen door ... dit:

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

Voor een beetje spanning heb ik een spoilerplaat over de oplossing gelegd. Beweeg de muis over het.

De zoekopdracht wordt ondersteund door de GiST-index en is snel, zelfs voor grote tabellen.

db<>viool hier (met meer voorbeelden)
Oude sqlfiddle

Als je de totale openingstijden (per winkel) wilt berekenen, is hier een recept:

  • Bereken werkuren tussen 2 datums in PostgreSQL

Index en prestaties

De insluitingsoperator voor bereiktypen kan worden ondersteund met een GiST of SP-GiST inhoudsopgave. Beide kunnen worden gebruikt om een ​​uitsluitingsbeperking te implementeren, maar alleen GiST ondersteunt indexen met meerdere kolommen:

Momenteel ondersteunen alleen de indextypen B-tree, GiST, GIN en BRIN indexen met meerdere kolommen.

En de volgorde van indexkolommen is van belang:

Een GiST-index met meerdere kolommen kan worden gebruikt met queryvoorwaarden die betrekking hebben op een subset van de kolommen van de index. Voorwaarden op extra kolommen beperken de invoer die door de index wordt geretourneerd, maar de voorwaarde in de eerste kolom is de belangrijkste om te bepalen hoeveel van de index moet worden gescand. Een GiST-index is relatief ondoeltreffend als de eerste kolom slechts een paar verschillende waarden heeft, zelfs als er veel verschillende waarden in extra kolommen zijn.

We hebben dus tegenstrijdige belangen hier. Voor grote tabellen zijn er veel meer verschillende waarden voor shop_id dan voor hours .

  • Een GiST-index met toonaangevende shop_id is sneller om te schrijven en om de uitsluitingsbeperking af te dwingen.
  • Maar we zoeken hours in onze vraag. Het zou beter zijn om die kolom eerst te hebben.
  • Als we shop_id moeten opzoeken in andere zoekopdrachten is een gewone btree-index veel sneller daarvoor.
  • Als klap op de vuurpijl vond ik een SP-GiST index op slechts hours om snelste te zijn voor de vraag.

Benchmark

Nieuwe test met Postgres 12 op een oude laptop. Mijn script om dummy-gegevens te genereren:

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;

Resultaten in ~ 141k willekeurig gegenereerde rijen, ~ 30k verschillende shop_id , ~ 12k verschillende hours . Tabelgrootte 8 MB.

Ik heb de uitsluitingsbeperking verwijderd en opnieuw gemaakt:

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 eerste is ~ 4x sneller voor deze distributie.

Daarnaast heb ik er nog twee getest op leesprestaties:

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

Na VACUUM FULL ANALYZE hoo; , Ik heb twee zoekopdrachten uitgevoerd:

  • Q1 :'s avonds laat, slechts 35 rijen gevonden
  • Q2 :'s middags vinden 4547 rijen .

Resultaten

Heb een alleen-index scan voor elk (behalve voor "geen index", natuurlijk):

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 en GiST zijn vergelijkbaar voor zoekopdrachten die weinig resultaten opleveren (GiST is zelfs sneller voor zeer paar).
  • SP-GiST schaalt beter met een groeiend aantal resultaten en is ook kleiner.

Als u veel meer leest dan u schrijft (typisch gebruiksscenario), houdt u dan aan de uitsluitingsbeperking zoals aan het begin gesuggereerd en maakt u een extra SP-GiST-index om de leesprestaties te optimaliseren.




  1. Hoe AT TIME ZONE werkt in PostgreSQL

  2. Oracle:meerdere tabelupdates => ORA-01779:kan geen kolom wijzigen die is toegewezen aan een niet-sleutelbewaarde tabel

  3. Tips en trucs voor het implementeren van op databaserollen gebaseerde toegangscontroles voor MariaDB

  4. MySQL draait rij naar dynamisch aantal kolommen