Como lidar com um plano de consulta incorreto causado pela igualdade exata no tipo de intervalo?

28

Estou executando uma atualização em que exijo uma igualdade exata em uma tstzrangevariável. ~ 1M linhas são modificadas e a consulta leva ~ 13 minutos. O resultado de EXPLAIN ANALYZEpode 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_rangeespera 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_statsvista 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_rangealgum 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_rangeacima 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).

  1. 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 .

  2. 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.

abeboparebop
fonte
1
E se você convertesse a condição de igualdade (a = b)em duas condições "contém" (a @> b AND b @> a):? O plano muda?
precisa saber é o seguinte
@ypercube: o plano muda substancialmente, embora ainda não seja o ideal - veja minha edição # 2.
Abeboparebop
1
Outra idéia seria adicionar um índice btree regular (lower(t_range),upper(t_range))desde que você verifique a igualdade.
precisa saber é o seguinte

Respostas:

9

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_timinge executando as consultas comEXPLAIN (ANALYZE, BUFFERS)

Os diferentes planos estão apresentando linhas a serem atualizadas em diferentes ordens. Um está em trip_idordem 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.

jjanes
fonte
Infelizmente, não consigo modificar track_io_timinge (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.
Abeboparebop
7

Não sei exatamente por que a seletividade de um predicado de igualdade é tão radicalmente superestimada pelo índice GiST na tstzrangecoluna. Embora isso permaneça interessante, parece irrelevante para o seu caso em particular.

Como você UPDATEmodifica 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 seu UPDATE.

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 o UPDATE. Isso ainda seria mais rápido do que manter o índice durante o período UPDATE.

Por UPDATEum terço de todas as linhas, provavelmente será bom descartar todos os outros índices - e recriá-los após o UPDATE. A única desvantagem: você precisa de privilégios adicionais e um bloqueio exclusivo sobre a mesa (apenas por um breve momento, se você usar CREATE 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á que tstzrangenã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:

Um tipo de faixa discreta deve ter uma função de canonização que esteja ciente do tamanho da etapa desejada para o tipo de elemento. A função de canonização é encarregada de converter valores equivalentes do tipo de intervalo para ter representações idênticas, em particular limites consistentemente inclusivos ou exclusivos. Se uma função de canonização não for especificada, os intervalos com formatação diferente sempre serão tratados como desiguais, mesmo que possam representar o mesmo conjunto de valores na realidade.

O embutido tipos de intervalo int4range, int8rangee daterangetodo o uso de uma forma canónica que inclui o limite inferior e o limite superior exclui; isto é, [). Os tipos de intervalo definidos pelo usuário podem usar outras convenções, no entanto.

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:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

E as consultas teriam que usar as mesmas expressões na WHEREcláusula.

Pode-se ficar tentado a indexar todo o valor convertido para text: (cast(t_range AS text))- mas essa expressão não ocorre IMMUTABLEporque a representação de texto dos timestamptzvalores depende da timezoneconfiguração atual . Você precisaria colocar etapas adicionais em uma IMMUTABLEfunção de invólucro que produza uma forma canônica e criar um índice funcional ...

Medidas adicionais / idéias alternativas

Se shape_dist_traveledjá pode ter o mesmo valor tt.shape_dist_traveledde mais de algumas linhas atualizadas (e você não confia em nenhum efeito colateral de seus UPDATEgatilhos semelhantes ...), você pode tornar sua consulta mais rápida excluindo atualizações vazias:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Obviamente, todos os conselhos gerais para otimização de desempenho se aplicam. O Wiki do Postgres é um bom ponto de partida.

VACUUM FULLseria um veneno para você, já que algumas tuplas mortas (ou espaço reservado por FILLFACTOR) são benéficas para o UPDATEdesempenho.

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:

Erwin Brandstetter
fonte