Otimizando consultas em vários timestamps (duas colunas)

96

Eu uso o PostgreSQL 9.1 no Ubuntu 12.04.

Preciso selecionar registros dentro de um intervalo de tempo: minha tabela time_limitspossui dois timestampcampos e uma integerpropriedade. Existem colunas adicionais na minha tabela real que não estão envolvidas com esta consulta.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Esta tabela contém aproximadamente 2 milhões de registros.

Consultas como as seguintes levaram uma quantidade enorme de tempo:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Então, tentei adicionar outro índice - o inverso do PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

Tive a impressão de que o desempenho melhorou: o tempo para acessar registros no meio da tabela parece ser mais razoável: algo entre 40 e 90 segundos.

Mas ainda há várias dezenas de segundos para valores no meio do intervalo de tempo. E mais duas vezes ao mirar o final da tabela (cronologicamente falando).

Tentei explain analyzepela primeira vez obter esse plano de consulta:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Veja os resultados em depesz.com.

O que eu poderia fazer para otimizar a pesquisa? Você pode ver todo o tempo gasto varrendo as duas colunas de carimbo de data / hora, uma vez id_phidefinido como 0. E eu não entendo a grande varredura (60 mil linhas!) Nos registros de data e hora. Eles não são indexados pela chave primária e idx_inversedeu adicionei?

Devo mudar de tipos de carimbo de data / hora para outra coisa?

Eu li um pouco sobre os índices GIST e GIN. Acho que eles podem ser mais eficientes em determinadas condições para tipos personalizados. É uma opção viável para o meu caso de uso?

Stephane Rolland
fonte
1
bem, são 45 anos. Não sei por que diz 45ms. Eu nem começaria a reclamar se isso fosse tão rápido quanto 45ms ... :-) Talvez um bug na saída do explicar analise. Ou talvez seja o momento da análise para executar. Não sei. Mas 40/50 segundos é o que eu medo.
Stephane Rolland
2
O tempo relatado na explain analyzesaída é o tempo que a consulta precisava no servidor . Se a sua consulta demorar 45 segundos, o tempo adicional será gasto transferindo os dados do banco de dados para o programa que está executando a consulta. Afinal, são 62682 linhas e se cada linha é grande (por exemplo, possui longas varcharou textcolunas), isso pode afetar o tempo de transferência. drasticamente.
precisa saber é o seguinte
@a_horse_with_no_name: rows=62682 rowsé a estimativa do planejador . A consulta retorna 0 linhas. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter
@ ErwinBrandstetter: ah, certo. Eu ignorei isso. Mas ainda assim eu nunca vi a saída de explicar analisar mentir sobre o tempo de execução.
a_horse_with_no_name

Respostas:

162

Para o Postgres 9.1 ou posterior:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

Na maioria dos casos, a ordem de classificação de um índice é pouco relevante. O Postgres pode retroceder praticamente o mais rápido possível. Mas, para consultas de intervalo em várias colunas, isso pode fazer uma enorme diferença. Intimamente relacionado:

Considere sua consulta:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

A ordem de classificação da primeira coluna id_phino índice é irrelevante. Uma vez que foi verificado para igualdade ( =), ele deve vir primeiro. Você acertou. Mais nesta resposta relacionada:

O Postgres pode pular rapidamente id_phi = 0e considere as duas colunas a seguir do índice correspondente. Eles são consultados com condições de intervalo de ordem de classificação invertida ( <=, >=). No meu índice, as linhas qualificadas são as primeiras. Deve ser a maneira mais rápida possível com o índice 1 da Árvore B :

  • Você deseja start_date_time <= something: o índice tem o carimbo de data e hora mais antigo primeiro.
    • Se estiver qualificado, verifique também a coluna 3.
      Continue até a primeira linha não se qualificar (super rápido).
  • Você deseja end_date_time >= something: o índice possui o carimbo de data e hora mais recente primeiro.
    • Se estiver qualificado, continue buscando linhas até a primeira não (super rápido).
      Continue com o próximo valor para a coluna 2.

O Postgres pode avançar ou retroceder. Da maneira que você teve o índice, ele precisa ler todas as linhas correspondentes nas duas primeiras colunas e depois filtrar na terceira. Certifique-se de ler o capítulo Índices eORDER BY o manual. Ele se encaixa muito bem na sua pergunta.

Quantas linhas correspondem nas duas primeiras colunas?
Apenas alguns estão start_date_timeperto do início do intervalo de tempo da tabela. Mas quase todas as linhas estão id_phi = 0no final cronológico da tabela! Portanto, o desempenho se deteriora com os horários de início posteriores.

Estimativas do planejador

O planejador estima rows=62682para sua consulta de exemplo. Desses, nenhum se qualifica ( rows=0). Você pode obter melhores estimativas se aumentar o destino das estatísticas da tabela. Para 2.000.000 linhas ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... pode pagar. Ou ainda mais. Mais nesta resposta relacionada:

Eu acho que você não precisa disso para id_phi(apenas alguns valores distintos, distribuídos uniformemente), mas para os registros de data e hora (muitos valores distintos, distribuídos de maneira desigual).
Também não acho que isso importe muito com o índice aprimorado.

CLUSTER / pg_repack

Se você quiser mais rápido, ainda assim, poderá otimizar a ordem física das linhas na sua tabela. Se você pode bloquear sua tabela exclusivamente por um curto período de tempo (por exemplo, fora do horário de expediente) para reescrever sua tabela e ordenar linhas de acordo com o índice:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Com acesso simultâneo, considere pg_repack , que pode fazer o mesmo sem bloqueio exclusivo.

De qualquer forma, o efeito é que menos blocos precisam ser lidos da tabela e tudo é pré-classificado. É um efeito único que se deteriora com o tempo, com gravações na tabela fragmentando a ordem de classificação física.

Índice GiST no Postgres 9.2+

1 Na página 9.2+, existe outra opção possivelmente mais rápida: um índice GiST para uma coluna de intervalo.

  • Existem tipos de intervalo integrados para timestampe timestamp with time zone: tsrange,tstzrange . Um índice btree normalmente é mais rápido para uma integercoluna adicional como id_phi. Menor e mais barato de manter também. Mas a consulta provavelmente ainda será mais rápida no geral com o índice combinado.

  • Altere sua definição de tabela ou use um índice de expressão .

  • Para o índice GiST com várias colunas em mãos, você também precisa do módulo adicional btree_gistinstalado (uma vez por banco de dados), que fornece às classes de operadores para incluir um integer.

O trio! Um índice GiST funcional de várias colunas :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Use o operador "contém intervalo"@> em sua consulta agora:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Índice SP-GiST no Postgres 9.3+

Um índice SP-GiST pode ser ainda mais rápido para esse tipo de consulta - exceto que, citando o manual :

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

Ainda é verdade no Postgres 12.
Você teria que combinar um spgistíndice apenas (tsrange(...))com um segundo btreeíndice (id_phi). Com a sobrecarga adicional, não tenho certeza se isso pode competir.
Resposta relacionada com uma referência para apenas uma tsrangecoluna:

Erwin Brandstetter
fonte
78
Devo dizer isso pelo menos apenas uma vez, que cada uma das suas respostas sobre SO e DBA são realmente de alto valor agregado / exprertise e, na maioria das vezes, as mais completas. Só para dizer uma vez: respeito!
Stephane Rolland
1
Merci bien! :) Então você obteve resultados mais rápidos?
Erwin Brandstetter
Eu tenho que deixar terminar a grande cópia em massa gerada a partir da minha consulta intensamente incômoda, para tornar o processo realmente lento, ele estava girando por horas antes de eu fazer a pergunta. Mas eu calculei e decidi deixá-lo girar até amanhã de manhã, estará terminado e a nova tabela pronta para ser preenchida amanhã. Eu tentei criar seu índice simultaneamente durante o trabalho, mas devido a muito acesso (eu acho), a criação do índice deve estar bloqueada. Repetirei esse mesmo tempo de teste novamente agora com a sua solução. Também observei como atualizar para 9.2 ;-) para o debian / ubuntu.
Stephane Rolland
2
@ StephaneRolland: ainda seria interessante o motivo pelo qual a saída de análise de explicação mostra 45 milissegundos enquanto você vê a consulta levando mais de 40 segundos.
a_horse_with_no_name
1
@ John: O Postgres pode percorrer um índice para frente ou para trás, mas não pode mudar de direção na mesma varredura. Idealmente, você tem todas as linhas qualificadas por nó primeiro (ou último), mas deve ter o mesmo alinhamento (predicados de consulta correspondentes) para todas as colunas para obter melhores resultados.
Erwin Brandstetter
5

A resposta de Erwin já é abrangente, no entanto:

Os tipos de intervalo para registros de data e hora estão disponíveis no PostgreSQL 9.1 com a extensão Temporal de Jeff Davis: https://github.com/jeff-davis/PostgreSQL-Temporal

Nota: possui recursos limitados (usa Timestamptz, e você só pode ter o estilo '[)' sobreposto). Além disso, existem muitas outras ótimas razões para atualizar para o PostgreSQL 9.2.

nathan-m
fonte
3

Você pode tentar criar o índice de várias colunas em uma ordem diferente:

primary key(id_phi, start_date_time,end_date_time);

Postei uma vez uma pergunta semelhante também relacionada à ordenação de índices em um índice de várias colunas. A chave está tentando usar primeiro as condições mais restritivas para reduzir o espaço de pesquisa.

Edit : Meu erro. Agora vejo que você já tem esse índice definido.

jap1968
fonte
Eu já tenho os dois índices. Exceto a chave primária é o outro, mas o índice que você propõe já existe, e é aquele que é usado se você olhar para a explicar:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland
1

Consegui aumentar rapidamente (de 1 segundo para 70ms)

Eu tenho uma tabela com agregações de muitas medições e muitos níveis ( lcoluna) (30s, 1m, 1h, etc), existem duas colunas ligadas ao intervalo: $spara início e $efim.

Criei dois índices de várias colunas: um para o início e outro para o fim.

Eu ajustei a consulta de seleção: selecione os intervalos em que o limite inicial é determinado. Além disso, selecione intervalos em que o limite final esteja em determinado intervalo.

O Explain mostra dois fluxos de linhas usando nossos índices com eficiência.

Índices:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Selecionar consulta:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Explicar:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

O truque é que os nós do plano contêm apenas linhas desejadas. Anteriormente, obtivemos milhares de linhas no nó do plano porque ele foi selecionado e all points from some point in time to the very end, em seguida, o próximo nó removeu as linhas desnecessárias.

Borovsky
fonte