Continuamos uma série de artigos dedicados ao estudo de formas pouco conhecidas para melhorar o desempenho de consultas PostgreSQL "aparentemente simples":
Não pense que eu não gosto muito de JOIN ... :)
Mas muitas vezes sem ele, o pedido é significativamente mais produtivo do que com ele. Portanto, hoje tentaremos nos
livrar completamente
do JOIN, que consome muitos recursos - com a ajuda de um dicionário.

A partir do PostgreSQL 12, algumas das situações descritas abaixo podem ser interpretadas de maneira um pouco diferente devido à não materialização do CTE por padrão . Esse comportamento pode ser revertido usando a chave MATERIALIZED
.
Muitos "fatos" em um vocabulário limitado
Vamos usar um aplicativo muito real - você precisa listar
as mensagens recebidas ou tarefas ativas com os remetentes:
25.01 | .. | . 22.01 | .. | : JOIN. 20.01 | .. | . 18.01 | .. | : JOIN . 16.01 | .. | .
No mundo abstrato, os autores de tarefas devem ser distribuídos igualmente entre todos os funcionários de nossa organização, mas, na realidade, as
tarefas vêm, em regra, de um número bastante limitado de pessoas - “dos superiores” na hierarquia ou “dos aliados” dos departamentos vizinhos (analistas, designers marketing ...).
Vamos supor que em nossa organização de 1000 pessoas, apenas 20 autores (geralmente menos ainda) definam tarefas para cada artista específico e
usem esse conhecimento para agilizar a solicitação "tradicional".
Mostramos as últimas 100 tarefas para um artista específico:
SELECT task.* , person.name FROM task LEFT JOIN person ON person.id = task.author_id WHERE owner_id = 777 ORDER BY task_date DESC LIMIT 100;
[veja em explicar.tensor.ru]Acontece que
1/3 do tempo todo e 3/4 das leituras das páginas de dados foram feitas apenas para pesquisar o autor 100 vezes - para cada tarefa exibida. Mas sabemos que dentre essas cem existem
apenas 20 diferentes - é possível usar esse conhecimento?
dicionário hstore
Usamos
o tipo hstore para gerar um "dicionário" de valor-chave:
CREATE EXTENSION hstore
Basta colocar o ID do autor e o nome dele no dicionário, para que possamos extrair mais tarde usando esta chave:
[veja em explicar.tensor.ru]Demorou
2 vezes menos tempo para obter informações sobre pessoas
e 7 vezes menos dados lidos ! Além de "engano", esses resultados também nos ajudaram a obter
a extração em massa de registros da tabela em uma única passagem usando
= ANY(ARRAY(...))
.
Entradas da tabela: serialização e desserialização
Mas e se precisarmos salvar no dicionário não um campo de texto, mas um registro inteiro? Nesse caso, a capacidade do PostgreSQL
de escrever uma tabela como um valor único nos ajudará:
... , dict AS ( SELECT hstore( array_agg(id)::text[] , array_agg(p)::text[]
Vejamos o que aconteceu aqui:
- Pegamos p como um alias para o registro completo da tabela de pessoas e montamos uma matriz a partir delas.
- Essa matriz de entradas foi reformulada em uma matriz de cadeias de texto (person [] :: text []) para colocá-la no dicionário hstore como uma matriz de valores.
- Ao receber o registro vinculado, o retiramos do dicionário por chave como uma sequência de texto.
- Precisamos transformar o texto no valor do tipo da tabela de pessoas (para cada tabela, o tipo com o mesmo nome é criado automaticamente).
- “Implantou” um registro digitado em colunas usando
(...).*
.
dicionário json
Mas esse truque, como aplicamos acima, não funcionará se não houver um tipo de tabela correspondente para fazer uma "desatenção". Exatamente a mesma situação surgirá, e se, como fonte de dados para serialização, tentarmos usar a
linha CTE, e não a tabela "real" .
Nesse caso, as
funções para trabalhar com json nos ajudarão:
... , p AS (
Deve-se notar que, ao descrever a estrutura de destino, não podemos listar todos os campos da cadeia de origem, mas apenas aqueles que realmente precisamos. Se tivermos uma tabela "nativa", é melhor usar a função
json_populate_record
.
Ainda temos acesso ao dicionário uma vez, mas os
custos da serialização de json- [de] são bastante altos , portanto, é razoável usar esse método apenas em alguns casos quando o CTE Scan "honesto" se mostra pior.
Teste de desempenho
Portanto, temos duas maneiras de serializar dados em um dicionário -
hstore / json_object . Além disso, as matrizes de chaves e valores também podem ser geradas de duas maneiras, com conversão interna ou externa em texto:
array_agg (i :: text) / array_agg (i) :: text [] .
Vamos verificar a eficácia de diferentes tipos de serialização usando um exemplo puramente sintético -
serializamos um número diferente de chaves :
WITH dict AS ( SELECT hstore( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, ...) i ) TABLE dict;
Script de avaliação: serialização WITH T AS ( SELECT * , ( SELECT regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (\d+\.\d+) ms$', '\1')::real et FROM ( SELECT array_agg(el) ea FROM dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$ explain analyze WITH dict AS ( SELECT hstore( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, $$ || (1 << v) || $$) i ) TABLE dict $$) T(el text) ) T ) et FROM generate_series(0, 19) v , LATERAL generate_series(1, 7) i ORDER BY 1, 2 ) SELECT v , avg(et)::numeric(32,3) FROM T GROUP BY 1 ORDER BY 1;

No PostgreSQL 11, até um tamanho de dicionário de 2 ^ 12 chaves, a
serialização no json leva menos tempo . A combinação de json_object e a conversão de tipo "interno" de
array_agg(i::text)
é a mais eficiente.
Agora vamos tentar ler o valor de cada chave 8 vezes - porque se você não acessa o dicionário, por que é necessário?
Script de avaliação: lendo de um dicionário WITH T AS ( SELECT * , ( SELECT regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (\d+\.\d+) ms$', '\1')::real et FROM ( SELECT array_agg(el) ea FROM dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$ explain analyze WITH dict AS ( SELECT json_object( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, $$ || (1 << v) || $$) i ) SELECT (TABLE dict) -> (i % ($$ || (1 << v) || $$) + 1)::text FROM generate_series(1, $$ || (1 << (v + 3)) || $$) i $$) T(el text) ) T ) et FROM generate_series(0, 19) v , LATERAL generate_series(1, 7) i ORDER BY 1, 2 ) SELECT v , avg(et)::numeric(32,3) FROM T GROUP BY 1 ORDER BY 1;

E ... já com cerca de
2 ^ 6 teclas, a leitura do dicionário json começa a perder para a leitura do hstore
várias vezes , para jsonb a mesma coisa acontece em 2 ^ 9.
Conclusões finais:
- se você precisar criar um JOIN com registros repetidos repetidamente - é melhor usar a "correspondência de tabela"
- se seu dicionário for pequeno e você ler um pouco dele - você pode usar json [b]
- em todos os outros casos, hstore + array_agg (i :: text) será mais eficiente