PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Por que uma pequena alteração no termo de pesquisa torna a consulta mais lenta?

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 . O Postgres é forçado a converter isso em um 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 :

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.