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

Udfør denne timers forespørgsel i PostgreSQL

Tabellayout

Re-design tabellen til at gemme åbningstider (åbningstider) som et sæt tsrange (interval af timestamp without time zone ) værdier. Kræver Postgres 9.2 eller nyere .

Vælg en tilfældig uge til at planlægge dine åbningstider. Jeg kan lide ugen:
1996-01-01 (mandag) til 1996-01-07 (søndag)
Det er det seneste skudår, hvor 1. januar passende er en mandag. Men det kan være en hvilken som helst tilfældig uge for denne sag. Bare vær konsekvent.

Installer det ekstra modul btree_gist først:

CREATE EXTENSION btree_gist;

Se:

  • Svarer til ekskluderingsbegrænsning sammensat af heltal og interval

Opret derefter tabellen sådan her:

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

Den en kolonne hours erstatter alle dine kolonner:

opens_on, closes_on, opens_at, closes_at

For eksempel åbningstider fra onsdag kl. 18.30 til torsdag kl. 05:00 UTC indtastes som:

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

Ekskluderingsbegrænsningen hoo_no_overlap forhindrer overlappende poster pr. butik. Det er implementeret med et GiST-indeks , hvilket også tilfældigvis understøtter vores forespørgsler. Overvej kapitlet "Indeks og ydeevne" nedenfor diskuterer indekseringsstrategier.

Kontrolbegrænsningen hoo_bounds_inclusive håndhæver inkluderende grænser for dine områder med to bemærkelsesværdige konsekvenser:

  • Et tidspunkt, der falder nøjagtigt på den nedre eller øvre grænse, er altid inkluderet.
  • Tilstødende tilmeldinger til den samme butik er i praksis forbudt. Med inklusiv grænser ville disse "overlappe", og udelukkelsesbegrænsningen ville give anledning til en undtagelse. Tilstødende poster skal i stedet flettes til en enkelt række. Undtagen når de ombrydes omkring midnat søndag , i så fald skal de opdeles i to rækker. Funktionen f_hoo_hours() nedenfor tager sig af dette.

Kontrolbegrænsningen hoo_standard_week håndhæver de ydre grænser for iscenesættelsesugen ved hjælp af "rækkevidde er indeholdt af" operatoren <@ .

Med inklusive grænser, skal du observere en hjørnekasse hvor tiden går rundt ved midnat søndag:

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

Du skal søge efter begge tidsstempler på én gang. Her er en relateret sag med eksklusiv øvre grænse, der ikke ville udvise denne mangel:

  • Forebyggelse af tilstødende/overlappende poster med EXCLUDE i PostgreSQL

Funktion f_hoo_time(timestamptz)

At "normalisere" et givet timestamp with time zone :

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 kun til Postgres 9.6 eller nyere.

Funktionen tager timestamptz og returnerer timestamp . Den tilføjer det forløbne interval for den respektive uge ($1 - date_trunc('week', $1) i UTC-tid til startpunktet for vores iscenesættelsesuge. (date + interval producerer timestamp .)

Funktion f_hoo_hours(timestamptz, timestamptz)

For at normalisere intervaller og opdele dem, der krydser man kl. 00:00. Denne funktion tager ethvert interval (som to timestamptz ) og producerer en eller to normaliserede tsrange værdier. Det dækker enhver juridisk input og forbyder resten:

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

For at INSERT en enkelt input række:

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

For enhver antal inputrækker:

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

Hver kan indsætte to rækker, hvis et område skal opdeles ved man kl. 00:00 UTC.

Forespørgsel

Med det tilpassede design, din hele store, komplekse, dyre forespørgsel kan erstattes med ... denne:

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

For lidt spænding satte jeg en spoilerplade over opløsningen. Flyt musen over det.

Forespørgslen er understøttet af nævnte GiST-indeks og hurtig, selv for store borde.

db<>spil her (med flere eksempler)
Gamle sqlfiddle

Ønsker du at beregne samlede åbningstider (pr. butik), er her en opskrift:

  • Beregn arbejdstimer mellem 2 datoer i PostgreSQL

Indeks og ydeevne

Indeslutningsoperatøren for områdetyper kan understøttes med en GiST eller SP-GiST indeks. Begge kan bruges til at implementere en ekskluderingsbegrænsning, men kun GiST understøtter indekser med flere kolonner:

I øjeblikket er det kun B-tree-, GiST-, GIN- og BRIN-indekstyperne, der understøtter indekser med flere kolonner.

Og rækkefølgen af ​​indekskolonner har betydning:

Et GiST-indeks med flere kolonner kan bruges med forespørgselsbetingelser, der involverer en hvilken som helst delmængde af indeksets kolonner. Betingelser for yderligere kolonner begrænser de poster, der returneres af indekset, men betingelsen i den første kolonne er den vigtigste for at bestemme, hvor meget af indekset, der skal scannes. Et GiST-indeks vil være relativt ineffektivt, hvis dets første kolonne kun har nogle få distinkte værdier, selvom der er mange distinkte værdier i yderligere kolonner.

Så vi har modstridende interesser her. For store borde vil der være mange flere forskellige værdier for shop_id end i hours .

  • Et GiST-indeks med førende shop_id er hurtigere at skrive og håndhæve udelukkelsesbegrænsningen.
  • Men vi søger efter hours i vores forespørgsel. Det ville være bedre at have den kolonne først.
  • Hvis vi skal slå shop_id op i andre forespørgsler er et almindeligt btree-indeks meget hurtigere til det.
  • Over det hele fandt jeg en SP-GiST indeks på kun hours at være hurtigst for forespørgslen.

Benchmark

Ny test med Postgres 12 på en gammel bærbar computer. Mit script til at generere dummy-data:

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;

Resulterer i ~ 141.000 tilfældigt genererede rækker, ~ 30.000 distinkt shop_id , ~ 12.000 forskellige hours . Bordstørrelse 8 MB.

Jeg droppede og genskabte ekskluderingsbegrænsningen:

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 første er ~ 4x hurtigere for denne distribution.

Derudover testede jeg to mere til læseydelse:

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

Efter VACUUM FULL ANALYZE hoo; , jeg kørte to forespørgsler:

  • 1. kvartal :sen nat, finder kun 35 rækker
  • Q2 :om eftermiddagen, finde 4547 rækker .

Resultater

Fik en kun-indeksscanning for hver (undtagen "intet indeks", selvfølgelig):

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 og GiST er på niveau med forespørgsler med få resultater (GiST er endnu hurtigere for meget få).
  • SP-GiST skalerer bedre med et stigende antal resultater og er også mindre.

Hvis du læser meget mere, end du skriver (typisk brug), skal du beholde udelukkelsesbegrænsningen som foreslået i starten og oprette et ekstra SP-GiST-indeks for at optimere læseydelsen.




  1. Eksternt Oracle-tabeleksempel

  2. Automatisk stigning i Oracle uden brug af en trigger

  3. Hvordan skriver man UTF-8-tegn ved hjælp af bulkinsert i SQL Server?

  4. Et følelsesløst logisk blik på SQL Server-navnekonventioner