Estou executando uma atualização em que exijo uma igualdade exata em uma tstzrange
variável. ~ 1M linhas são modificadas e a consulta leva ~ 13 minutos. O resultado de EXPLAIN ANALYZE
pode ser visto aqui , e os resultados reais são extremamente diferentes daqueles estimados pelo planejador de consultas. O problema é que a verificação do índice t_range
espera que uma única linha seja retornada.
Isso parece estar relacionado ao fato de que as estatísticas sobre os tipos de intervalo são armazenadas de maneira diferente das de outros tipos. Observando a pg_stats
vista da coluna, n_distinct
é -1 e outros campos (por exemplo most_common_vals
, most_common_freqs
) estão vazios.
No entanto, deve haver estatísticas armazenadas em t_range
algum lugar. Uma atualização extremamente semelhante, na qual eu uso um 'dentro' em t_range, em vez de uma igualdade exata, leva cerca de 4 minutos para executar e usa um plano de consulta substancialmente diferente (veja aqui ). O segundo plano de consulta faz sentido para mim, porque todas as linhas da tabela temporária e uma fração substancial da tabela de histórico serão usadas. Mais importante, o planejador de consultas prevê um número aproximadamente correto de linhas para o filtro ativado t_range
.
A distribuição de t_range
é um pouco incomum. Estou usando esta tabela para armazenar o estado histórico de outra tabela e as alterações na outra tabela ocorrem de uma só vez em grandes despejos, portanto, não há muitos valores distintos de t_range
. Aqui estão as contagens correspondentes a cada um dos valores exclusivos de t_range
:
t_range | count
-------------------------------------------------------------------+---------
["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00") | 994676
["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") | 36791
["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00") | 1000403
["2014-06-27 07:00:00+00",infinity) | 36791
["2014-08-01 07:00:01+00",infinity) | 999753
As contagens das distintas t_range
acima estão completas; portanto, a cardinalidade é de ~ 3M (dos quais ~ 1M serão afetados por uma consulta de atualização).
Por que a consulta 1 tem um desempenho muito pior do que a consulta 2? No meu caso, a consulta 2 é um bom substituto, mas se uma igualdade exata de intervalo fosse realmente necessária, como eu poderia fazer o Postgres usar um plano de consulta mais inteligente?
Definição de tabela com índices (eliminando colunas irrelevantes):
Column | Type | Modifiers
---------------------+-----------+------------------------------------------------------------------------------
history_id | integer | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
t_range | tstzrange | not null
trip_id | text | not null
stop_sequence | integer | not null
shape_dist_traveled | real |
Indexes:
"gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
"gtfs_stop_times_history_t_range" gist (t_range)
"gtfs_stop_times_history_trip_id" btree (trip_id)
Consulta 1:
UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;
Consulta 2:
UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;
O Q1 atualiza 999753 linhas e o Q2 atualiza 999753 + 36791 = 1036544 (ou seja, a tabela temporária é tal que todas as linhas correspondentes à condição de intervalo de tempo são atualizadas).
Eu tentei esta consulta em resposta ao comentário do @ ypercube :
Consulta 3:
UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;
O plano de consulta e os resultados (veja aqui ) foram intermediários entre os dois casos anteriores (~ 6 minutos).
05/02/2016 EDIT
Não tendo mais acesso aos dados após 1,5 anos, criei uma tabela de teste com a mesma estrutura (sem índices) e cardinalidade semelhante. A resposta de jjanes propôs que a causa poderia ser a ordem da tabela temporária usada para a atualização. Não pude testar a hipótese diretamente porque não tenho acesso track_io_timing
(usando o Amazon RDS).
Os resultados gerais foram muito mais rápidos (por um fator de vários). Suponho que isso se deva à remoção dos índices, consistente com a resposta de Erwin .
Nesse caso de teste, as consultas 1 e 2 basicamente levaram a mesma quantidade de tempo, porque ambas usaram a junção de mesclagem. Ou seja, eu não consegui acionar o que estava causando o Postgres escolher a junção de hash, portanto, não tenho clareza sobre por que o Postgres estava escolhendo a junção de hash com desempenho ruim em primeiro lugar.
(a = b)
em duas condições "contém"(a @> b AND b @> a)
:? O plano muda?(lower(t_range),upper(t_range))
desde que você verifique a igualdade.Respostas:
A maior diferença de tempo em seus planos de execução está no nó superior, o próprio UPDATE. Isso sugere que a maior parte do seu tempo vai para E / S durante a atualização. Você pode verificar isso ativando
track_io_timing
e executando as consultas comEXPLAIN (ANALYZE, BUFFERS)
Os diferentes planos estão apresentando linhas a serem atualizadas em diferentes ordens. Um está em
trip_id
ordem e o outro está na ordem que eles estiverem fisicamente presentes na tabela temporária.A tabela que está sendo atualizada parece ter sua ordem física correlacionada com a coluna trip_id, e a atualização de linhas nessa ordem leva a padrões de E / S eficientes com leituras de leitura antecipada / sequencial. Embora a ordem física da tabela temporária pareça levar a muitas leituras aleatórias.
Se você pode adicionar um
order by trip_id
à instrução que criou a tabela temporária, isso pode resolver o problema.O PostgreSQL não leva em consideração os efeitos do pedido de IO ao planejar a operação UPDATE. (Diferentemente das operações SELECT, onde são consideradas). Se o PostgreSQL fosse mais inteligente, ele perceberia que um plano produz uma ordem mais eficiente ou interporia um nó de classificação explícito entre a atualização e seu nó filho, para que a atualização fosse alimentada com linhas na ordem ctid.
Você está certo de que o PostgreSQL faz um péssimo trabalho estimando a seletividade das associações de igualdade em intervalos. No entanto, isso está relacionado apenas tangencialmente ao seu problema fundamental. Uma consulta mais eficiente na parte selecionada da atualização pode acidentalmente alimentar as linhas na atualização adequada em uma ordem melhor, mas, se for o caso, isso se deve principalmente à sorte.
fonte
track_io_timing
e (já faz um ano e meio!) Não tenho mais acesso aos dados originais. No entanto, testei sua teoria criando tabelas com o mesmo esquema e tamanho semelhante (milhões de linhas) e executando duas atualizações diferentes - uma na qual a tabela de atualização temporária foi classificada como a tabela original e outra na qual foi classificada quase aleatoriamente. Infelizmente, as duas atualizações demoram aproximadamente a mesma quantidade de tempo, o que implica que a ordem da tabela de atualização não afeta essa consulta.Não sei exatamente por que a seletividade de um predicado de igualdade é tão radicalmente superestimada pelo índice GiST na
tstzrange
coluna. Embora isso permaneça interessante, parece irrelevante para o seu caso em particular.Como você
UPDATE
modifica um terço (!) De todas as linhas existentes da 3M, um índice não ajuda em nada . Pelo contrário, a atualização incremental do índice, além da tabela, adicionará um custo substancial ao seuUPDATE
.Apenas mantenha sua simples consulta 1 . A solução simples e radical é eliminar o índice antes do
UPDATE
. Se você precisar para outros fins, recrie-o após oUPDATE
. Isso ainda seria mais rápido do que manter o índice durante o períodoUPDATE
.Por
UPDATE
um terço de todas as linhas, provavelmente será bom descartar todos os outros índices - e recriá-los após oUPDATE
. A única desvantagem: você precisa de privilégios adicionais e um bloqueio exclusivo sobre a mesa (apenas por um breve momento, se você usarCREATE INDEX CONCURRENTLY
).A idéia de @ ypercube de usar uma btree em vez do índice GiST parece boa em princípio. Mas não para um terço de todas as linhas (onde nenhum índice é bom para começar), e não apenas
(lower(t_range),upper(t_range))
, já quetstzrange
não é um tipo de intervalo discreto.A maioria dos tipos de intervalos discretos possui uma forma canônica, o que simplifica o conceito de "igualdade": o limite inferior e superior do valor na forma canônica o define. A documentação:
Não é esse o caso
tstzrange
, onde a inclusão do limite superior e inferior precisa ser considerada para a igualdade. Um possível índice de btree teria que estar em:E as consultas teriam que usar as mesmas expressões na
WHERE
cláusula.Pode-se ficar tentado a indexar todo o valor convertido para
text
:- mas essa expressão não ocorre(cast(t_range AS text))
IMMUTABLE
porque a representação de texto dostimestamptz
valores depende datimezone
configuração atual . Você precisaria colocar etapas adicionais em umaIMMUTABLE
função de invólucro que produza uma forma canônica e criar um índice funcional ...Medidas adicionais / idéias alternativas
Se
shape_dist_traveled
já pode ter o mesmo valortt.shape_dist_traveled
de mais de algumas linhas atualizadas (e você não confia em nenhum efeito colateral de seusUPDATE
gatilhos semelhantes ...), você pode tornar sua consulta mais rápida excluindo atualizações vazias:Obviamente, todos os conselhos gerais para otimização de desempenho se aplicam. O Wiki do Postgres é um bom ponto de partida.
VACUUM FULL
seria um veneno para você, já que algumas tuplas mortas (ou espaço reservado porFILLFACTOR
) são benéficas para oUPDATE
desempenho.Com tantas linhas atualizadas e se você puder pagar (sem acesso simultâneo ou outras dependências), pode ser ainda mais rápido escrever uma tabela completamente nova em vez de atualizar no local. Instruções nesta resposta relacionada:
fonte