sql >> Databasteknik >  >> RDS >> PostgreSQL

Utför denna tidsfråga i PostgreSQL

Tabelllayout

Omforma tabellen för att lagra öppettider (öppningstider) som en uppsättning tsrange (intervall för timestamp without time zone ) värden. Kräver Postgres 9.2 eller senare .

Välj en slumpmässig vecka för att visa dina öppettider. Jag gillar veckan:
1996-01-01 (måndag) till 1996-01-07 (söndag)
Det är det senaste skottåret där den 1 januari bekvämtvis råkar vara en måndag. Men det kan vara vilken slumpmässig vecka som helst för det här fallet. Var bara konsekvent.

Installera tilläggsmodulen btree_gist först:

CREATE EXTENSION btree_gist;

Se:

  • Motsvarar uteslutningsbegränsning som består av heltal och intervall

Skapa sedan tabellen så här:

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 kolumn hours ersätter alla dina kolumner:

opens_on, closes_on, opens_at, closes_at

Till exempel öppettider från onsdag, 18:30 till torsdag, 05:00 UTC anges som:

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

Uteslutningsrestriktionen hoo_no_overlap förhindrar överlappande poster per butik. Det implementeras med ett GiST-index , vilket också råkar stödja våra frågor. Tänk på kapitlet "Index och prestanda" nedan diskuterar indexeringsstrategier.

Kontrollbegränsningen hoo_bounds_inclusive upprätthåller inkluderande gränser för dina intervall, med två anmärkningsvärda konsekvenser:

  • En tidpunkt som faller på den nedre eller övre gränsen exakt ingår alltid.
  • Angränsande poster för samma butik är i praktiken otillåtna. Med inkluderande gränser skulle dessa "överlappa" och uteslutningsbegränsningen skulle ge upphov till ett undantag. Intilliggande poster måste istället slås samman till en enda rad. Förutom när de sluter runt midnatt på söndag , i så fall måste de delas upp i två rader. Funktionen f_hoo_hours() nedan tar hand om detta.

Kontrollbegränsningen hoo_standard_week upprätthåller de yttre gränserna för iscensättningsveckan med operatorn "räckvidd finns av" <@ .

Med inklusive gränser måste du observera ett hörnfall där tiden går runt vid midnatt på 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 måste söka efter båda tidsstämplarna samtidigt. Här är ett relaterat fall med exklusiv övre gräns som inte skulle uppvisa denna brist:

  • Förhindra intilliggande/överlappande poster med EXCLUDE i PostgreSQL

Funktion f_hoo_time(timestamptz)

Att "normalisera" en given 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 endast för Postgres 9.6 eller senare.

Funktionen tar timestamptz och returnerar timestamp . Den lägger till det förflutna intervallet för respektive vecka ($1 - date_trunc('week', $1) i UTC-tid till startpunkten för vår iscensättningsvecka. (date + interval producerar timestamp .)

Funktion f_hoo_hours(timestamptz, timestamptz)

För att normalisera avstånden och dela de som passerar mån 00:00. Denna funktion tar vilket intervall som helst (som två timestamptz). ) och producerar ett eller två normaliserade tsrange värden. Den täcker alla juridisk input och tillåter inte 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$;

För att INSERT en singel inmatningsrad:

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 alla antal inmatningsrader:

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

Var och en kan infoga två rader om ett intervall behöver delas vid mån 00:00 UTC.

Fråga

Med den anpassade designen, din hela stora, komplexa, dyra fråga kan ersättas med ... detta:

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

För lite spänning satte jag en spoilerplatta över lösningen. Flytta musen över det.

Frågan backas upp av nämnda GiST-index och är snabb, även för stora bord.

db<>spela här (med fler exempel)
Gammal sqlfiddle

Om du vill räkna ut totala öppettider (per butik) kommer här ett recept:

  • Beräkna arbetstid mellan 2 datum i PostgreSQL

Index och prestanda

Inneslutningsoperatorn för intervalltyper kan stödjas med en GiST eller SP-GiST index. Båda kan användas för att implementera en uteslutningsrestriktion, men bara GiST stöder multikolumnindex:

För närvarande är det bara indextyperna B-tree, GiST, GIN och BRIN som stöder multikolumnindex.

Och ordningen på indexkolumnerna har betydelse:

Ett GiST-index med flera kolumner kan användas med frågevillkor som involverar vilken delmängd som helst av indexets kolumner. Villkor för ytterligare kolumner begränsar posterna som returneras av indexet, men villkoret i den första kolumnen är det viktigaste för att avgöra hur mycket av indexet som behöver skannas. Ett GiST-index kommer att vara relativt ineffektivt om dess första kolumn bara har ett fåtal distinkta värden, även om det finns många distinkta värden i ytterligare kolumner.

Så vi har motstridiga intressen här. För stora tabeller kommer det att finnas många fler distinkta värden för shop_id än för hours .

  • Ett GiST-index med ledande shop_id är snabbare att skriva och att upprätthålla undantagsbegränsningen.
  • Men vi söker efter hours i vår fråga. Att ha den kolumnen först skulle vara bättre.
  • Om vi ​​behöver slå upp shop_id i andra frågor är ett vanligt btree-index mycket snabbare för det.
  • På det hela hittade jag en SP-GiST index på bara hours att vara snabbast för frågan.

Benchmark

Nytt test med Postgres 12 på en gammal bärbar dator.Mitt skript för att generera dummydata:

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;

Resultat i ~ 141 000 slumpmässigt genererade rader, ~ 30 000 distinkt shop_id , ~ 12 000 distinkta hours . Bordsstorlek 8 MB.

Jag släppte och återskapade uteslutningsbegrä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örsta är ~ 4 gånger snabbare för denna distribution.

Dessutom testade jag två till för läsprestanda:

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; , jag körde två frågor:

  • Q1 :sen kväll, hittar bara 35 rader
  • Q2 :på eftermiddagen, hitta 4547 rader .

Resultat

Fick en endast indexskanning för varje (förutom "inget index", naturligtvis):

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 och GiST är lika för frågor som får få resultat (GiST är ännu snabbare för mycket få).
  • SP-GiST skalar bättre med ett växande antal resultat och är också mindre.

Om du läser mycket mer än du skriver (typiskt användningsfall), behåll uteslutningsbegränsningen som föreslagits i början och skapa ett ytterligare SP-GiST-index för att optimera läsprestanda.




  1. Introduktion till Oracle Databas Backup

  2. SQLite och databasinitiering

  3. PostgreSQL 13:Låt inte slots döda din primära

  4. Exportera SQLite-databas till en CSV-fil