PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Wykonaj zapytanie o godziny pracy w PostgreSQL

Układ tabeli

Przeprojektuj stół, aby przechowywać godziny otwarcia (godziny pracy) jako zestaw tsrange (zakres timestamp without time zone ) wartości. Wymaga Postgresa 9.2 lub nowszego .

Wybierz losowy tydzień, aby ustawić godziny otwarcia. Lubię tydzień:
1996-01-01 (poniedziałek) do 07.01.1996 (niedziela)
To ostatni rok przestępny, w którym 1 stycznia wypada w poniedziałek. Ale w tym przypadku może to być dowolny losowy tydzień. Po prostu bądź konsekwentny.

Zainstaluj dodatkowy moduł btree_gist po pierwsze:

CREATE EXTENSION btree_gist;

Zobacz:

  • Równoważne z ograniczeniem wykluczenia złożonym z liczby całkowitej i zakresu

Następnie utwórz tabelę w ten sposób:

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

jeden kolumna hours zastępuje wszystkie Twoje kolumny:

opens_on, closes_on, opens_at, closes_at

Na przykład godziny pracy od środa, 18:30 do czwartku, 05:00 UTC są wprowadzane jako:

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

Ograniczenie wykluczenia hoo_no_overlap zapobiega nakładaniu się wpisów na sklep. Jest zaimplementowany z indeksem GiST , które również wspierają nasze zapytania. Rozważ rozdział „Indeks i wydajność” poniżej omawiając strategie indeksowania.

Ograniczenie sprawdzające hoo_bounds_inclusive wymusza inkluzywne granice dla twoich zakresów, z dwiema godnymi uwagi konsekwencjami:

  • Zawsze uwzględniany jest punkt w czasie przypadający dokładnie na dolną lub górną granicę.
  • Sąsiadujące wpisy dla tego samego sklepu są skutecznie zabronione. W przypadku granic inkluzywnych te „nachodziłyby na siebie”, a ograniczenie wykluczenia powodowałoby wyjątek. Sąsiednie wpisy należy zamiast tego scalić w jeden wiersz. Chyba że zawijają się w niedzielę o północy , w takim przypadku muszą być podzielone na dwa rzędy. Funkcja f_hoo_hours() poniżej zajmuje się tym.

Ograniczenie sprawdzania hoo_standard_week wymusza zewnętrzne granice tygodnia pomostowego przy użyciu operatora „zakres zawiera się w” <@ .

Z włącznie granice, musisz obserwować przypadek narożny gdzie czas się kończy o północy w niedzielę:

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

Musisz wyszukać oba znaczniki czasu jednocześnie. Oto powiązana sprawa z ekskluzywnym górna granica, która nie wykazałaby tej wady:

  • Zapobieganie sąsiednim/nakładającym się wpisom za pomocą EXCLUDE w PostgreSQL

Funkcja f_hoo_time(timestamptz)

Aby "znormalizować" dowolny podany 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 tylko dla Postgresa 9.6 lub nowszego.

Funkcja przyjmuje timestamptz i zwraca timestamp . Dodaje interwał, który upłynął w danym tygodniu ($1 - date_trunc('week', $1) w czasie UTC do punktu rozpoczęcia naszego tygodnia inscenizacji. (date + interval generuje timestamp .)

Funkcja f_hoo_hours(timestamptz, timestamptz)

Aby znormalizować zakresy i podzielić te, które przecinają Mon 00:00. Ta funkcja zajmuje dowolny interwał (jako dwa timestamptz ) i tworzy jeden lub dwa znormalizowane tsrange wartości. Obejmuje dowolne legalny wkład i nie zezwala na resztę:

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

Aby INSERT singiel wiersz wejściowy:

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

Dla dowolnych liczba wierszy wejściowych:

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

Każdy może wstawić dwa wiersze, jeśli zakres wymaga podziału o godz. 00:00 UTC.

Zapytanie

Dzięki dostosowanemu projektowi całe duże, złożone i kosztowne zapytanie można zastąpić ... to:

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

Dla trochę suspensu nałożyłem na roztwór płytę spoilera. Przesuń mysz nad to.

Zapytanie jest obsługiwane przez wspomniany indeks GiST i jest szybkie, nawet w przypadku dużych tabel.

db<>graj tutaj (więcej przykładów)
Stary sqlfiddle

Jeśli chcesz obliczyć łączne godziny otwarcia (na sklep), oto przepis:

  • Oblicz godziny pracy między 2 datami w PostgreSQL

Indeks i wydajność

Operator zawierania dla typów zasięgu może być obsługiwany przez GiST lub SP-GiST indeks. Każdego można użyć do zaimplementowania ograniczenia wykluczenia, ale tylko GiST obsługuje indeksy wielokolumnowe:

Obecnie tylko typy indeksów B-drzewo, GiST, GIN i BRIN obsługują indeksy wielokolumnowe.

A kolejność kolumn indeksu ma znaczenie:

Wielokolumnowy indeks GiST może być używany z warunkami zapytania, które obejmują dowolny podzbiór kolumn indeksu. Warunki w dodatkowych kolumnach ograniczają wpisy zwracane przez indeks, ale warunek w pierwszej kolumnie jest najważniejszy dla określenia, jaka część indeksu musi zostać przeskanowana. Indeks GiST będzie stosunkowo nieefektywny, jeśli jego pierwsza kolumna ma tylko kilka odrębnych wartości, nawet jeśli w dodatkowych kolumnach jest wiele odrębnych wartości.

Mamy więc sprzeczne interesy tutaj. W przypadku dużych tabel będzie dużo więcej odrębnych wartości dla shop_id niż przez hours .

  • Indeks GiST z wiodącym shop_id jest szybszy w pisaniu i egzekwowaniu ograniczenia wykluczenia.
  • Ale szukamy hours w naszym zapytaniu. Lepiej byłoby mieć tę kolumnę jako pierwszą.
  • Jeśli musimy wyszukać shop_id w innych zapytaniach zwykły indeks btree jest do tego znacznie szybszy.
  • Na domiar złego znalazłem SP-GiST indeksuj tylko hours być najszybszym dla zapytania.

Wzorzec

Nowy test z Postgresem 12 na starym laptopie.Mój skrypt do generowania fikcyjnych danych:

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;

Wyniki w ~141k losowo wygenerowanych wierszy, ~30k odrębnych shop_id , ~ 12 tys. odrębnych hours . Rozmiar stołu 8 MB.

Usunąłem i odtworzyłem ograniczenie wykluczenia:

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 pierwszy jest ~ 4x szybszy dla tej dystrybucji.

Ponadto przetestowałem jeszcze dwa pod kątem wydajności odczytu:

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

Po VACUUM FULL ANALYZE hoo; , uruchomiłem dwa zapytania:

  • Pierwszy kwartał :późna noc, znajdowanie tylko 35 wierszy
  • II kwartał :po południu, znajdowanie 4547 wierszy .

Wyniki

Otrzymałem skanowanie tylko z indeksem dla każdego (oczywiście z wyjątkiem "bez indeksu"):

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 i GiST są takie same w przypadku zapytań, które znajdują niewiele wyników (GiST jest jeszcze szybszy w przypadku bardzo kilka).
  • SP-GiST skaluje się lepiej wraz ze wzrostem liczby wyników, a także jest mniejszy.

Jeśli czytasz dużo więcej niż piszesz (typowy przypadek użycia), zachowaj ograniczenie wykluczenia zgodnie z sugestią na początku i utwórz dodatkowy indeks SP-GiST, aby zoptymalizować wydajność odczytu.




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Wyświetlanie obrazu z bazy PostgreSQL, bajta

  2. Jak uzyskać aktualną datę i godzinę z przesunięciem strefy czasowej w PostgreSQL?

  3. Jak wyodrębnić rok i miesiąc od daty w PostgreSQL bez użycia funkcji to_char()?

  4. Dynamiczna funkcja zapytań Postgres

  5. funkcje tablicy biginteger