PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Execute esta consulta de horas de operação no PostgreSQL

Layout da tabela


Redesenhe a tabela para armazenar o horário de funcionamento (horário de funcionamento) como um conjunto de tsrange (intervalo de timestamp without time zone ) valores. Requer Postgres 9.2 ou posterior .

Escolha uma semana aleatória para organizar seu horário de funcionamento. Eu gosto da semana:
1996-01-01 (segunda-feira) para 1996-01-07 (domingo)
Esse é o ano bissexto mais recente em que 1º de janeiro convenientemente é uma segunda-feira. Mas pode ser qualquer semana aleatória para este caso. Basta ser consistente.

Instale o módulo adicional btree_gist primeiro:
CREATE EXTENSION btree_gist;

Ver:
  • Equivalente à restrição de exclusão composta por inteiro e intervalo

Em seguida, crie a tabela assim:
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]')
);

O um coluna hours substitui todas as suas colunas:
opens_on, closes_on, opens_at, closes_at

Por exemplo, horário de funcionamento a partir de quarta-feira, 18h30 até quinta-feira, 05:00 UTC são inseridos como:
'[1996-01-03 18:30, 1996-01-04 05:00]'

A restrição de exclusão hoo_no_overlap impede entradas sobrepostas por loja. Ele é implementado com um índice GiST , que também dá suporte às nossas consultas. Considere o capítulo "Índice e desempenho" abaixo discutindo estratégias de indexação.

A restrição de verificação hoo_bounds_inclusive impõe limites inclusivos para seus intervalos, com duas consequências dignas de nota:
  • Um ponto no tempo que cai exatamente no limite inferior ou superior é sempre incluído.
  • Entradas adjacentes para a mesma loja não são permitidas. Com limites inclusivos, eles se "sobreporiam" e a restrição de exclusão geraria uma exceção. As entradas adjacentes devem ser mescladas em uma única linha. Exceto quando eles envolvem a meia-noite de domingo , nesse caso eles devem ser divididos em duas linhas. A função f_hoo_hours() abaixo cuida disso.

A restrição de verificação hoo_standard_week impõe os limites externos da semana de preparação usando o operador "intervalo contido por" <@ .

Com inclusivo limites, você deve observar um caso de canto onde o tempo termina na meia-noite de domingo:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
 Mon 00:00 = Sun 24:00 (= next Mon 00:00)

Você tem que procurar os dois timestamps de uma só vez. Aqui está um caso relacionado com exclusivo limite superior que não exibiria essa deficiência:
  • Evitando entradas adjacentes/sobrepostas com EXCLUDE no PostgreSQL

Função f_hoo_time(timestamptz)


Para "normalizar" qualquer 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 apenas para Postgres 9.6 ou posterior.

A função recebe timestamptz e retorna timestamp . Adiciona o intervalo decorrido da respectiva semana ($1 - date_trunc('week', $1) em hora UTC para o ponto de partida da nossa semana de preparação. (date + interval produz timestamp .)

Função f_hoo_hours(timestamptz, timestamptz)


Para normalizar os intervalos e dividir aqueles que cruzam Seg 00:00. Esta função leva qualquer intervalo (como dois timestamptz ) e produz um ou dois tsrange normalizados valores. Abrange qualquer entrada legal e não permite o resto:
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$;

Para INSERT um único linha de entrada:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');

Para qualquer número de linhas de entrada:
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);

Cada um pode inserir duas linhas se um intervalo precisar ser dividido em Mon 00:00 UTC.

Consulta


Com o design ajustado, toda sua consulta grande, complexa e cara pode ser substituído por ... isto:

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

Para um pouco de suspense, coloquei uma placa de spoiler sobre a solução. Mova o mouse isso.

A consulta é apoiada pelo referido índice GiST e rápida, mesmo para tabelas grandes.

db<>mexa aqui (com mais exemplos)
Old sqlfiddle

Se você quiser calcular o total de horas de funcionamento (por loja), aqui está uma receita:
  • Calcular horas de trabalho entre 2 datas no PostgreSQL

Índice e desempenho


O operador de contenção para tipos de intervalo pode ser compatível com um GiST ou SP-GiST índice. Qualquer um pode ser usado para implementar uma restrição de exclusão, mas apenas o GiST suporta índices de várias colunas:

Atualmente, apenas os tipos de índice B-tree, GiST, GIN e BRIN suportam índices de várias colunas.

E a ordem das colunas de índice é importante:

Um índice GiST de várias colunas pode ser usado com condições de consulta que envolvem qualquer subconjunto das colunas do índice. As condições em colunas adicionais restringem as entradas retornadas pelo índice, mas a condição na primeira coluna é a mais importante para determinar quanto do índice precisa ser verificado. Um índice GiST será relativamente ineficaz se sua primeira coluna tiver apenas alguns valores distintos, mesmo que haja muitos valores distintos em colunas adicionais.

Portanto, temos interesses conflitantes aqui. Para tabelas grandes, haverá muito mais valores distintos para shop_id do que para hours .
  • Um índice GiST com shop_id principal é mais rápido escrever e aplicar a restrição de exclusão.
  • Mas estamos pesquisando hours em nossa consulta. Ter essa coluna primeiro seria melhor.
  • Se precisarmos procurar shop_id em outras consultas, um índice btree simples é muito mais rápido para isso.
  • Para completar, encontrei um SP-GiST indexe em apenas hours para ser mais rápido para a consulta.

Referência


Novo teste com Postgres 12 em um laptop antigo.Meu script para gerar dados fictícios:
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;

Resulta em ~ 141 mil linhas geradas aleatoriamente, ~ 30 mil shop_id distintos , ~ 12.000 hours distintas . Tamanho da tabela 8 MB.

Eu larguei e recriei a restrição de exclusão:
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 primeiro é ~ 4x mais rápido para esta distribuição.

Além disso, testei mais dois para desempenho de leitura:
CREATE INDEX hoo_hours_gist_idx   on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours);  -- !!

Após VACUUM FULL ANALYZE hoo; , executei duas consultas:
  • 1º trimestre :tarde da noite, encontrando apenas 35 linhas
  • 2º trimestre :à tarde, encontrar 4547 linhas .

Resultados


Obteve uma varredura somente de índice para cada (exceto para "sem índice", é claro):
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 e GiST estão no mesmo nível para consultas que encontram poucos resultados (GiST é ainda mais rápido para muito poucos).
  • O SP-GiST é melhor dimensionado com um número crescente de resultados e também é menor.

Se você lê muito mais do que escreve (caso de uso típico), mantenha a restrição de exclusão conforme sugerido no início e crie um índice SP-GiST adicional para otimizar o desempenho de leitura.