PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Изпълнете тази заявка за работни часове в PostgreSQL

Оформление на таблицата

Препроектирайте таблицата за съхраняване на работното време (часове на работа) като набор от tsrange (диапазон от timestamp without time zone ) стойности. Изисква Postgres 9.2 или по-нова версия .

Изберете произволна седмица, за да поставите работното си време. Харесвам седмицата:
1996-01-01 (понеделник) до 1996-01-07 (неделя)
Това е най-новата високосна година, в която 1 януари удобно е понеделник. Но това може да бъде произволна седмица за този случай. Просто бъдете последователни.

Инсталирайте допълнителния модул btree_gist първо:

CREATE EXTENSION btree_gist;

Вижте:

  • Еквивалентно на ограничение за изключване, съставено от цяло число и диапазон

След това създайте таблицата по следния начин:

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

единият колона hours замества всичките ви колони:

opens_on, closes_on, opens_at, closes_at

Например работно време от сряда, 18:30 до четвъртък, 05:00 ч. UTC се въвеждат като:

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

Ограничението за изключване hoo_no_overlap предотвратява припокриването на вписванията на магазин. Реализира се с индекс GiST , което също подкрепя нашите запитвания. Помислете за главата „Индекс и производителност“ по-долу се обсъждат стратегиите за индексиране.

Ограничението за проверка hoo_bounds_inclusive налага приобщаващи граници за вашите диапазони с две забележителни последици:

  • Винаги се включва момент от време, попадащ точно върху долната или горната граница.
  • Съседните записи за един и същи магазин на практика са забранени. С включващи граници, те биха се „припокривали“ и ограничението за изключване би предизвикало изключение. Вместо това съседните записи трябва да бъдат обединени в един ред. Освен когато те увиват около полунощ в неделя , като в този случай те трябва да бъдат разделени на два реда. Функцията f_hoo_hours() по-долу се грижи за това.

Ограничението за проверка hoo_standard_week налага външните граници на етапната седмица, като използва оператора <@ "диапазонът се съдържа от" .

Свключително граници, трябва да спазвате ъглов случай където времето приключва в неделя полунощ:

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

Трябва да потърсите и двете времеви марки наведнъж. Ето един свързан случай сизключително горна граница, която не показва този недостатък:

  • Предотвратяване на съседни/припокриващи се записи с EXCLUDE в PostgreSQL

Функция f_hoo_time(timestamptz)

За да "нормализирате" произволен 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 само за Postgres 9.6 или по-нова версия.

Функцията приема timestamptz и връща timestamp . Той добавя изтеклия интервал от съответната седмица ($1 - date_trunc('week', $1) по UTC до началната точка на нашата постановителна седмица. (date + interval произвежда timestamp .)

Функция f_hoo_hours(timestamptz, timestamptz)

За нормализиране на диапазони и разделяне на тези, които пресичат поне 00:00. Тази функция приема произволен интервал (като два timestamptz ) и произвежда един или два нормализирани tsrange стойности. Покривавсякото законно въвеждане и забранява останалите:

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

За INSERT единична ред за въвеждане:

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

За всякакви брой входни редове:

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

Всеки може да вмъкне два реда, ако диапазонът се нуждае от разделяне в понеделник 00:00 UTC.

Запитване

С коригирания дизайн, цялата ви голяма, сложна, скъпа заявка може да бъде заменен с ... това:

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

За малко напрежение поставих спойлерна плоча върху разтвора. Преместете мишката над то.

Заявката е подкрепена от посочения GiST индекс и е бърза, дори за големи таблици.

db<>цигулка тук (с още примери)
Стар sqlfiddle

Ако искате да изчислите общо работно време (за магазин), ето една рецепта:

  • Изчислете работното време между 2 дати в PostgreSQL

Индекс и производителност

Операторът за ограничаване за типове диапазони може да се поддържа с GiST или SP-GiST индекс. И двете могат да се използват за прилагане на ограничение за изключване, но само GiST поддържа многоколонни индекси:

Понастоящем само индексите B-tree, GiST, GIN и BRIN поддържат индекси с няколко колони.

И редът на индексните колони има значение:

GiST индекс с много колони може да се използва с условия на заявка, които включват всяко подмножество от колоните на индекса. Условията в допълнителните колони ограничават записите, върнати от индекса, но условието на първата колона е най-важното за определяне каква част от индекса трябва да бъде сканирана. GiST индексът ще бъде относително неефективен, ако първата му колона има само няколко различни стойности, дори ако има много различни стойности в допълнителни колони.

Така че имаме конфликтни интереси тук. За големи таблици ще има много повече различни стойности за shop_id отколкото за hours .

  • Индекс GiST с водещ shop_id е по-бърз за писане и за налагане на ограничението за изключване.
  • Но ние търсим hours в нашето запитване. Ще бъде по-добре първо да имате тази колона.
  • Ако трябва да потърсим shop_id в други заявки обикновен индекс на btree е много по-бърз за това.
  • Като капак намерих SP-GiST индекс само за hours да бъденай-бързи за заявката.

Сравнение

Нов тест с Postgres 12 на стар лаптоп. Моят скрипт за генериране на фиктивни данни:

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;

Резултати до ~ 141 000 произволно генерирани реда, ~ 30 000 различни shop_id , ~ 12k различни hours . Размер на таблицата 8 MB.

Изхвърлих и пресъздадох ограничението за изключване:

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 първото е ~ 4 пъти по-бързо за тази дистрибуция.

Освен това тествах още две за производителност на четене:

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

След VACUUM FULL ANALYZE hoo; , изпълних две заявки:

  • Q1 :късно през нощта, намирам само 35 реда
  • Q2 :следобед, намиране на 4547 реда .

Резултати

Получих сканиране само за индекс за всеки (с изключение на "без индекс", разбира се):

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 и GiST са на ниво за заявки, намиращи малко резултати (GiST е дори по-бърз за много малко).
  • SP-GiST се мащабира по-добре с нарастващ брой резултати и също е по-малък.

Ако четете много повече, отколкото пишете (типичен случай на употреба), запазете ограничението за изключване, както беше предложено в началото, и създайте допълнителен SP-GiST индекс, за да оптимизирате производителността на четене.




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Pandas записване на рамка от данни в друга postgresql схема

  2. Изберете първия ред във всяка група GROUP BY?

  3. Вземете Century от дата в PostgreSQL

  4. MIN() Функция в PostgreSQL

  5. Защо postgres не създава базата данни?