Otimizando uma consulta 'mais recente' no Postgres em 20 milhões de linhas


10

Minha tabela é a seguinte:

    Column             |    Type           |    
-----------------------+-------------------+
 id                    | integer           | 
 source_id             | integer           | 
 timestamp             | integer           | 
 observation_timestamp | integer           | 
 value                 | double precision  | 

existem índices em source_id, timestamp e em uma combinação de timestamp e id ( CREATE INDEX timeseries_id_timestamp_combo_idx ON timeseries (id, timeseries DESC NULLS LAST))

Existem 20 milhões de linhas (OK, 120 milhões, mas 20 milhões com source_id = 1). Tem muitas entradas para o mesmo timestampcom variação observation_timestamp, que descrevem uma valueocorrência em timestamprelatada ou observada em observation_timestamp. por exemplo, a temperatura prevista para amanhã às 14:00, conforme previsto hoje às 12h.

Idealmente, esta tabela faz algumas coisas bem:

  • lote inserindo novas entradas, às vezes 100K por vez
  • selecionando dados observados para intervalos de tempo ("quais são as previsões de temperatura de janeiro a março")
  • selecionando dados observados para intervalos de tempo como observados a partir de um certo ponto ("qual é a visão das previsões de temperatura para janeiro até março, como pensávamos em 1º de novembro")

O segundo é o que é central para esta questão.

Os dados na tabela teriam a seguinte aparência

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
2   1           1531084900  1531082900              1111
3   1           1531085900  1531083900              8888
4   1           1531085900  1531082900              7777
5   1           1531086900  1531082900              5555

e uma saída da consulta teria a seguinte aparência (apenas a linha do último observação_timestamp representada)

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
3   1           1531085900  1531083900              8888
5   1           1531086900  1531082900              5555

Já consultei algum material antes para otimizar essas consultas, a saber

... com sucesso limitado.

Eu considerei criar uma tabela separada timestampnela, para que seja mais fácil fazer referência lateralmente, mas devido à cardinalidade relativamente alta daqueles que duvido que eles me ajudem - além disso, estou preocupado que isso dificulte a realização batch inserting new entries.


Estou analisando três consultas e todas elas me dão um desempenho ruim

  • CTE recursiva com junção LATERAL
  • Função de janela
  • DISTINCT ON

(Estou ciente de que eles não fazem a mesma coisa no momento, mas servem como boas ilustrações do tipo de consulta, tanto quanto eu vejo.)

CTE recursiva com junção LATERAL

WITH RECURSIVE cte AS (
    (
        SELECT ts
        FROM timeseries ts
        WHERE source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    UNION ALL
    SELECT (
        SELECT ts1
        FROM timeseries ts1
        WHERE id > (c.ts).id
        AND source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    FROM cte c
    WHERE (c.ts).id IS NOT NULL
)
SELECT (ts).*
FROM cte
WHERE (ts).id IS NOT NULL
ORDER BY (ts).id;

Atuação:

Sort  (cost=164999681.98..164999682.23 rows=100 width=28)
  Sort Key: ((cte.ts).id)
  CTE cte
    ->  Recursive Union  (cost=1653078.24..164999676.64 rows=101 width=52)
          ->  Subquery Scan on *SELECT* 1  (cost=1653078.24..1653078.26 rows=1 width=52)
                ->  Limit  (cost=1653078.24..1653078.25 rows=1 width=60)
                      ->  Sort  (cost=1653078.24..1702109.00 rows=19612304 width=60)
                            Sort Key: ts.id, ts.timestamp DESC NULLS LAST
                            ->  Bitmap Heap Scan on timeseries ts  (cost=372587.92..1555016.72 rows=19612304 width=60)
                                  Recheck Cond: (source_id = 1)
                                  ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                        Index Cond: (source_id = 1)
          ->  WorkTable Scan on cte c  (cost=0.00..16334659.64 rows=10 width=32)
                Filter: ((ts).id IS NOT NULL)
                SubPlan 1
                  ->  Limit  (cost=1633465.94..1633465.94 rows=1 width=60)
                        ->  Sort  (cost=1633465.94..1649809.53 rows=6537435 width=60)
                              Sort Key: ts1.id, ts1.timestamp DESC NULLS LAST
                              ->  Bitmap Heap Scan on timeseries ts1  (cost=369319.21..1600778.77 rows=6537435 width=60)
                                    Recheck Cond: (source_id = 1)
                                    Filter: (id > (c.ts).id)
                                    ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                          Index Cond: (source_id = 1)
  ->  CTE Scan on cte  (cost=0.00..2.02 rows=100 width=28)
        Filter: ((ts).id IS NOT NULL)

(somente EXPLAIN, EXPLAIN ANALYZEnão foi possível concluir, levou> 24 horas para concluir a consulta)

Função de janela

WITH summary AS (
  SELECT ts.id, ts.source_id, ts.value,
    ROW_NUMBER() OVER(PARTITION BY ts.timestamp ORDER BY ts.observation_timestamp DESC) AS rn
  FROM timeseries ts
  WHERE source_id = 1
)
SELECT s.*
FROM summary s
WHERE s.rn = 1;

Atuação:

CTE Scan on summary s  (cost=5530627.97..5971995.66 rows=98082 width=24) (actual time=150368.441..226331.286 rows=88404 loops=1)
  Filter: (rn = 1)
  Rows Removed by Filter: 20673704
  CTE summary
    ->  WindowAgg  (cost=5138301.13..5530627.97 rows=19616342 width=32) (actual time=150368.429..171189.504 rows=20762108 loops=1)
          ->  Sort  (cost=5138301.13..5187341.98 rows=19616342 width=24) (actual time=150368.405..165390.033 rows=20762108 loops=1)
                Sort Key: ts.timestamp, ts.observation_timestamp DESC
                Sort Method: external merge  Disk: 689752kB
                ->  Bitmap Heap Scan on timeseries ts  (cost=372675.22..1555347.49 rows=19616342 width=24) (actual time=2767.542..50399.741 rows=20762108 loops=1)
                      Recheck Cond: (source_id = 1)
                      Rows Removed by Index Recheck: 217784
                      Heap Blocks: exact=48415 lossy=106652
                      ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2757.245..2757.245 rows=20762630 loops=1)
                            Index Cond: (source_id = 1)
Planning time: 0.186 ms
Execution time: 234883.090 ms

DISTINCT ON

SELECT DISTINCT ON (timestamp) *
FROM timeseries
WHERE source_id = 1
ORDER BY timestamp, observation_timestamp DESC;

Atuação:

Unique  (cost=5339449.63..5437531.34 rows=15991 width=28) (actual time=112653.438..121397.944 rows=88404 loops=1)
  ->  Sort  (cost=5339449.63..5388490.48 rows=19616342 width=28) (actual time=112653.437..120175.512 rows=20762108 loops=1)
        Sort Key: timestamp, observation_timestamp DESC
        Sort Method: external merge  Disk: 770888kB
        ->  Bitmap Heap Scan on timeseries  (cost=372675.22..1555347.49 rows=19616342 width=28) (actual time=2091.585..56109.942 rows=20762108 loops=1)
              Recheck Cond: (source_id = 1)
              Rows Removed by Index Recheck: 217784
              Heap Blocks: exact=48415 lossy=106652
              ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2080.054..2080.054 rows=20762630 loops=1)
                    Index Cond: (source_id = 1)
Planning time: 0.132 ms
Execution time: 161651.006 ms

Como devo estruturar meus dados, existem verificações que não deveriam estar lá, é geralmente possível obter essas consultas para ~ 1s (em vez de ~ 120s)?

Existe uma maneira diferente de consultar os dados para obter os resultados desejados?

Se não, qual infraestrutura / arquitetura diferente eu deveria estar olhando?


O que você basicamente quer é uma varredura de índice solto ou ignorar varredura. Esses estão chegando em breve. Você pode aplicar o patch agora se você quer confusão com ele postgresql-archive.org/Index-Skip-Scan-td6025532.html é apenas um mês de idade = P
— Evan Carroll

Vivendo no limite @EvanCarroll = P - isso parece um pouco cedo para mim, considerando que estou usando o Postgres no Azure nem mesmo possível.
— Pepijn Schoen

Mostre os planos EXPLAIN ANALYZE sem os LIMITs (já que é isso que precisa ser otimizado), mas com as alterações que recomendei na minha primeira resposta. Mas sem os LIMITs, acho que você está pedindo uma quantidade impossível de trabalho a ser realizado em ~ 1s. Talvez você possa pré-calcular algumas coisas.
— jjanes

@ jjanes absolutamente - obrigado pela sugestão. Eu removi o LIMITda pergunta agora e adicionei a saída com EXPLAIN ANALYZE(apenas EXPLAINna recursiveparte)
— Pepijn Schoen

Respostas:


1

Com sua consulta CTE recursiva, a final ORDER BY (ts).idé desnecessária, pois a CTE as cria automaticamente nessa ordem. A remoção que deve tornar a consulta muito mais rápida, pode parar mais cedo, em vez de gerar 20.180.572 linhas, apenas para eliminar todos, exceto 500. Além disso, a criação do índice (source_id, id, timestamp desc nulls last)deve melhorá-lo ainda mais.

Para as outras duas consultas, aumentar work_mem o suficiente para que os bitmaps caibam na memória (para se livrar dos blocos de heap com perdas) ajudaria alguns. Mas não tanto quanto índices personalizados, como (source_id, "timestamp", observation_timestamp DESC)ou melhor ainda, para verificações apenas de índice (source_id, "timestamp", observation_timestamp DESC, value, id).


Obrigado pela sugestão - certamente analisarei a indexação personalizada como você sugere. O LIMIT 500objetivo era limitar a saída, mas no código de produção isso não acontece. Vou editar minha postagem para refletir isso.
— Pepijn Schoen

Na ausência do LIMIT, os índices podem ser muito menos eficazes. Mas ainda vale a pena tentar.
— jjanes

Você está correto - com as LIMITe suas sugestões, atualmente a execução é 356.482 ms( Index Scan using ix_timeseries_source_id_timestamp_observation_timestamp on timeseries (cost=0.57..62573201.42 rows=18333374 width=28) (actual time=174.098..356.097 rows=2995 loops=1)), mas sem LIMITé como antes. Como eu também aproveitaria o Index Scancaso e não o Bitmap Index/Heap Scan?
— Pepijn Schoen
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.