Suponha as seguintes relações:
- match(match_id)
- event(match_id, seq, gt, ...)
Existem os seguintes índices:
- match(match_id)
- event(match_id, seq)
Notas adicionais:
- gt está aumentando monotonicamente
- Para uma determinada partida, tenho uma coleção de eventos que acontecem em um horário 'gt' específico
- tanto a partida quanto o evento são visualizações de tapete.
- Item da lista
Estou usando o postgresql 13.1
Meu objetivo é criar uma consulta CTE RECURSIVA que calcule o delta entre um evento e o próximo, porém acho isso muito lento. Embora isso possa ser resolvido praticamente com uma auto-junção, não estou interessado nisso, quero descobrir por que meu CTE está lento. Acredito que não deve ser tão lento.
Mais números:
- número de partidas é 400
- cada partida tem uma média de 541 eventos
Minha consulta CTE RECURSIVA é a seguinte:
WITH RECURSIVE
delta_gts AS (
SELECT m.match_id, 1 AS seq, 0 AS gt, 0 AS delta
FROM matches m
UNION
SELECT dgt.match_id, ev.seq AS seq, ev.gt AS gt, (ev.gt - dgt.gt) AS delta
FROM delta_gts dgt
JOIN events ev ON ev.match_id = dgt.match_id AND ev.seq = (dgt.seq + 1)
)
SELECT * FROM delta_gts g
Outras notas que também tentei adicionando o seguinte (apenas para uma partida):
WHERE g.match_id = 'ita_1672780'
e descubro no plano que não há pushdown de predicado. Eu acho que isso foi implementado no pgsql 13.1
Este é o plano:
QUERY PLAN
CTE Scan on delta_gts g (cost=160601.44..161032.40 rows=21548 width=76) (actual time=173.940..354185.831 rows=220268 loops=1)
" Buffers: shared hit=5453034 read=596370, temp read=1340253 written=1581611"
CTE delta_gts
-> Recursive Union (cost=0.00..160601.44 rows=21548 width=76) (actual time=173.931..353944.926 rows=220268 loops=1)
" Buffers: shared hit=5453034 read=596370, temp read=1340253 written=1580590"
-> Seq Scan on netcastingdocument_matches m (cost=0.00..10.08 rows=408 width=28) (actual time=173.917..174.265 rows=408 loops=1)
Buffers: shared hit=6
-> Hash Join (cost=14121.22..16016.04 rows=2114 width=76) (actual time=259.550..305.356 rows=190 loops=1158)
Hash Cond: ((dgt.match_id = ev.match_id) AND ((dgt.seq + 1) = ev.seq))
" Buffers: shared hit=5453028 read=596370, temp read=1340253 written=1580590"
-> WorkTable Scan on delta_gts dgt (cost=0.00..81.60 rows=4080 width=72) (actual time=0.005..0.067 rows=190 loops=1158)
-> Hash (cost=8106.89..8106.89 rows=288289 width=24) (actual time=257.949..257.949 rows=288323 loops=1158)
Buckets: 65536 Batches: 8 Memory Usage: 2484kB
" Buffers: shared hit=5453022 read=596370, temp written=1565616"
-> Seq Scan on netcastingdocument_events ev (cost=0.00..8106.89 rows=288289 width=24) (actual time=0.016..92.171 rows=288323 loops=1158)
Buffers: shared hit=5453022 read=596370
Planning:
Buffers: shared hit=107
Planning Time: 50.290 ms
JIT:
Functions: 13
" Options: Inlining false, Optimization false, Expressions true, Deforming true"
" Timing: Generation 4.108 ms, Inlining 0.000 ms, Optimization 19.158 ms, Emission 154.531 ms, Total 177.796 ms"
Execution Time: 355489.930 ms
Considerações:
- Ele não está usando o índice (match_id, seq) na tabela de eventos quando a parte recursiva do CTE é executada.
- Desabilitar o seqscan faz o truque, pois usará o índice para eventos.
Após alguma investigação, parece que o problema é que um SeqScan está sendo executado para procurar o próximo evento que não está correto na minha situação.
Pode haver várias causas; Não posso ter certeza, porque você não postou a
EXPLAIN (ANALYZE, BUFFERS)
saída para ambas as execuções.O PostgreSQL pode estimar incorretamente as contagens de linhas. Executar
ANALYZE
como você fez é uma boa abordagem aqui, mas em uma CTE recursiva as contagens de linhas geralmente são difíceis de prever e é difícil corrigir essas estimativas.Se você não se importa com um truque desagradável, você pode tentar adicionar outra condição de junção supérflua para fazer o PostgreSQL pensar que o resultado terá menos linhas:
O PostgreSQL pode precificar uma varredura de índice muito alta, o que o induz a escolher uma varredura sequencial e uma junção de hash em vez de uma junção de loop aninhado.
Se você tiver um SSD como disco, deve diminuir
random_page_cost
para 1 ou 1.1 para dar ao otimizador do PostgreSQL uma ideia de que as varreduras de índice não são quatro vezes mais caras que as varreduras sequenciais.Se você tiver RAM suficiente, deve definir
effective_cache_size
um valor alto o suficiente para que o PostgreSQL saiba que os dados provavelmente estão armazenados em cache. Isso também reduzirá o custo de uma verificação de índice.por que não usar a função de janela LEAD() ou LAG() que fará o que você deseja. E se puder obter a ordem das linhas de um índice, não precisará fazer nenhuma classificação.
Se você quiser a diferença entre uma linha e a anterior, use "value-lag(value,1)" ; também lag() recebe um parâmetro padrão, então se você quiser que o primeiro seja 0 em vez de NULL, use lag(value,1,0::FLOAT). Não parece funcionar se o tipo não for explicitamente convertido.
Agora, a pergunta inicial...
UNION remove linhas duplicadas. Como não haverá linhas duplicadas, pois as novas linhas adicionadas por cada consulta recursiva são diferentes das anteriores, isso é um desperdício de CPU, então eu a substituí por UNION ALL, que não faz o trabalho extra. Isso o torna cerca de 2x mais rápido.
Com UNION e UNION ALL, eu tenho praticamente o mesmo plano que o seu, exceto que é muito mais rápido. Então, mesma consulta, mesmo plano, velocidade diferente, isso é estranho.
A grande diferença são seus usos de hash: Lotes: 8 Uso de memória: 2484kB
E o meu usa apenas um lote. Então eu configurei work_mem para 2 MB (estava em 64 MB) e também obtive um hash multi-lote, que era tão lento quanto sua consulta.
Parece que, quando o hash pode ser feito em um lote, o postgres o fará apenas uma vez para toda a consulta, mas se tiver que ser feito em vários lotes, ele o refaz para cada iteração da consulta recursiva, ou seja cerca de 500 vezes. Isso deve explicar por que é lento. Aparentemente, o planejador não está ciente disso, então ele escolhe o hashjoin.
Usar
set enable_hashjoin to 'f';
antes da consulta faz com que ela use o índice. Isso é muito mais lento do que um hash de 1 bucket feito uma vez e muito mais rápido do que um hash refeito para cada iteração.Mas realmente, a solução adequada é usar uma função de janela. Ele também irá lidar adequadamente com uma condição como WHERE match_id=...