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?

Pepijn Schoen
fonte
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).

jjanes
fonte
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