Por quê?
A razão é isto:
Consulta rápida:
-> Hash Left Join (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)
Consulta lenta:
-> Hash Left Join (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)
Estender o padrão de pesquisa por outro caractere faz com que o Postgres assuma ainda menos acertos. (Normalmente, esta é uma estimativa razoável.) O Postgres obviamente não tem estatísticas precisas o suficiente (nenhuma, na verdade, continue lendo) para esperar o mesmo número de acessos que você realmente obtém.
Isso causa uma mudança para um plano de consulta diferente, que é ainda menos ideal para o real número de ocorrências
rows=1129
. Solução
Assumindo o Postgres 9.5 atual, uma vez que não foi declarado.
Uma maneira de melhorar a situação é criar um índice de expressão na expressão no predicado. Isso faz com que o Postgres colete estatísticas para a expressão real, o que pode ajudar a consulta mesmo que o próprio índice não seja usado para a consulta . Sem o índice, não há nenhuma estatística para a expressão em tudo. E se feito corretamente o índice pode ser usado para a consulta, isso é ainda muito melhor. Mas há vários problemas com sua expressão atual:
unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) como unaccent('%vicen%' )
Considere esta consulta atualizada, com base em algumas suposições sobre suas definições de tabela não divulgadas:
SELECT e.id
, (SELECT count(*) FROM imgitem
WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
, e.ano, e.mes, e.dia
, e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
, pl.pltag, e.inpa, e.det, d.ano anodet
, format('%s (%s)', p.abrev, p.prenome) AS determinador
, d.tax
, coalesce(v.val,v.valf) || ' ' || vu.unit AS altura
, coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
, d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
, ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM pess p -- reorder!
JOIN det d ON d.detby = p.id -- INNER JOIN !
LEFT JOIN tax tf ON tf.oldfam = d.fam
LEFT JOIN tax tg ON tg.oldgen = d.gen
LEFT JOIN tax ts ON ts.oldsp = d.sp
LEFT JOIN tax ti ON ti.oldinf = d.inf -- unused, see @joop's comment
LEFT JOIN esp e ON e.det = d.id
LEFT JOIN loc l ON l.id = e.loc
LEFT JOIN var v ON v.esp = e.id AND v.key = 265
LEFT JOIN varunit vu ON vu.id = v.unit
LEFT JOIN var v1 ON v1.esp = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id = v1.unit
LEFT JOIN pl ON pl.id = e.pl
WHERE f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%') OR
f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');
Pontos principais
Por que
f_unaccent()
? Porque unaccent()
não pode ser indexado. Leia isso:Eu usei a função descrita lá para permitir o seguinte (recomendado!) trigrama funcional multicoluna GIN índice :
CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);
Se você não estiver familiarizado com índices de trigramas, leia isto primeiro:
E possivelmente:
Certifique-se de executar a versão mais recente do Postgres (atualmente 9.5). Houve melhorias substanciais nos índices GIN. E você estará interessado em melhorias no pg_trgm 1.2, programado para ser lançado com o próximo Postgres 9.6:
Declarações preparadas são uma maneira comum de executar consultas com parâmetros (especialmente com texto da entrada do usuário). O Postgres precisa encontrar um plano que funcione melhor para qualquer parâmetro. Adicione curingas como constantes para o termo de pesquisa assim:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
(
'vicenti'
seria substituído por um parâmetro.) Então o Postgres sabe que estamos lidando com um padrão que não está ancorado à esquerda nem à direita - o que permitiria estratégias diferentes. Resposta relacionada com mais detalhes:Ou talvez replaneje a consulta para cada termo de pesquisa (possivelmente usando SQL dinâmico em uma função). Mas certifique-se de que o tempo de planejamento não está consumindo nenhum ganho de desempenho possível.
O
ONDE
condição nas colunas em pess
contradiz o LEFT JOIN
INNER JOIN
. O que é pior, a junção vem tarde na árvore de junção. E como o Postgres não pode reordenar suas junções (veja abaixo), isso pode se tornar muito caro. Mova a tabela para o primeiro posição no FROM
cláusula para eliminar linhas antecipadamente. Seguindo LEFT JOIN
s não elimina nenhuma linha por definição. Mas com tantas tabelas é importante mover junções que podem multiplicar fileiras até o final. Você está juntando 13 tabelas, 12 delas com
LEFT JOIN
que deixa 12!
combinações possíveis - ou 11! * 2!
se pegarmos o LEFT JOIN
em conta que é realmente um INNER JOIN
. Isso é também muitos para o Postgres avaliar todas as permutações possíveis para o melhor plano de consulta. Leia sobre join_collapse_limit
:- Consulta de amostra para mostrar erro de estimativa de cardinalidade no PostgreSQL
- SQL INNER JOIN em várias tabelas igual à sintaxe WHERE
A configuração padrão para
join_collapse_limit
é 8 , o que significa que o Postgres não tentará reordenar as tabelas em seu FROM
cláusula e a ordem das tabelas é relevante . Uma maneira de contornar isso seria dividir a parte crítica de desempenho em um CTE como @joop comentou . Não defina
join_collapse_limit
muito maior ou os tempos para planejamento de consultas envolvendo muitas tabelas unidas irão se deteriorar. Sobre sua data concatenada chamado
dados
:cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||' -'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) como dados
Supondo você constrói a partir de três colunas numéricas para ano, mês e dia, que são definidas como
NOT NULL
, use isso em vez disso:e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
Sobre o
FM
modificador de padrão de modelo:Mas, na verdade, você deve armazenar a data como tipo de dados
date
começar com. Também simplificado:
format('%s (%s)', p.abrev, p.prenome) AS determinador
Não tornará a consulta mais rápida, mas é muito mais limpa. Consulte
format()
. As primeiras coisas por último, todos os conselhos usuais para otimização de desempenho se aplica:
Se você acertar tudo isso, verá consultas muito mais rápidas para todos padrões.