PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Pourquoi un léger changement dans le terme de recherche ralentit-il autant la requête ?

Pourquoi ?

La raison est-ce :

Requête rapide :

->  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)) ~~* (...)

Requête lente :

->  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 (...)

L'extension du modèle de recherche par un autre caractère oblige Postgres à supposer encore moins de résultats. (Généralement, il s'agit d'une estimation raisonnable.) Postgres n'a évidemment pas de statistiques suffisamment précises (aucune, en fait, continuez à lire) pour s'attendre au même nombre de visites que vous obtenez réellement.

Cela provoque un basculement vers un plan de requête différent, ce qui est encore moins optimal pour le réel nombre de visites rows=1129 .

Solution

En supposant que la version actuelle de Postgres 9.5 n'a pas été déclarée.

Une façon d'améliorer la situation est de créer un index d'expression sur l'expression dans le prédicat. Cela permet à Postgres de collecter des statistiques pour l'expression réelle, ce qui peut aider la requête même si l'index lui-même n'est pas utilisé pour la requête . Sans l'index, il n'y a pas de statistiques pour l'expression du tout. Et si c'est bien fait, l'index peut être utilisé pour la requête, c'est encore mieux. Mais il y a plusieurs problèmes avec votre expression courante :

unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')

Considérez cette requête mise à jour, basée sur certaines hypothèses à propos de vos définitions de table non divulguées :

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' || '%');

Points majeurs

Pourquoi f_unaccent() ? Parce que unaccent() ne peut pas être indexé. Lisez ceci :

J'ai utilisé la fonction décrite ici pour autoriser le trigramme fonctionnel multicolonne suivant (recommandé !) GIN index :

CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);

Si vous n'êtes pas familier avec les index trigrammes, lisez d'abord ceci :

Et éventuellement :

Assurez-vous d'exécuter la dernière version de Postgres (actuellement 9.5). Des améliorations substantielles ont été apportées aux indices GIN. Et vous serez intéressé par les améliorations de pg_trgm 1.2, dont la sortie est prévue avec le prochain Postgres 9.6 :

Déclarations préparées sont un moyen courant d'exécuter des requêtes avec des paramètres (en particulier avec du texte provenant d'une entrée utilisateur). Postgres doit trouver un plan qui fonctionne le mieux pour un paramètre donné. Ajouter des caractères génériques comme constantes au terme de recherche comme ceci :

f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')

('vicenti' serait remplacé par un paramètre.) Postgres sait donc que nous avons affaire à un modèle qui n'est ancré ni à gauche ni à droite - ce qui permettrait différentes stratégies. Réponse connexe avec plus de détails :

Ou peut-être replanifier la requête pour chaque terme de recherche (éventuellement en utilisant du SQL dynamique dans une fonction). Mais assurez-vous que le temps de planification ne ronge aucun gain de performances possible.

Le WHERE condition sur les colonnes dans pess contredit le LEFT JOIN . Postgres est obligé de convertir cela en un INNER JOIN . Pire encore, la jointure arrive tard dans l'arbre de jointure. Et puisque Postgres ne peut pas réorganiser vos jointures (voir ci-dessous), cela peut devenir très coûteux. Déplacer le tableau vers le premier position dans le FROM clause pour éliminer les lignes plus tôt. Après LEFT JOIN s n'éliminent aucune ligne par définition. Mais avec autant de tables, il est important de déplacer les jointures qui pourraient se multiplier rangées jusqu'à la fin.

Vous rejoignez 13 tables, dont 12 avec LEFT JOIN ce qui laisse 12! combinaisons possibles - ou 11! * 2! si on prend celui LEFT JOIN en compte c'est vraiment un INNER JOIN . C'est aussi beaucoup pour que Postgres évalue toutes les permutations possibles pour le meilleur plan de requête. En savoir plus sur join_collapse_limit :

Le paramètre par défaut pour join_collapse_limit est 8 , ce qui signifie que Postgres n'essaiera pas de réorganiser les tables dans votre FROM clause et l'ordre des tables est pertinent .

Une façon de contourner ce problème serait de diviser la partie critique pour les performances en un CTE comme @joop a commenté . Ne définissez pas join_collapse_limit beaucoup plus élevés ou les temps de planification des requêtes impliquant de nombreuses tables jointes se détérioreront.

À propos de votre date concaténée nommé data :

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)) as data

En supposant vous construisez à partir de trois colonnes numériques pour l'année, le mois et le jour, qui sont définis NOT NULL , utilisez ceci à la place :

e.ano::text || to_char(e.mes2, 'FM"-"00')
            || to_char(e.dia,  'FM"-"00') AS data

À propos de la FM modificateur de modèle de modèle :

Mais vraiment, vous devriez stocker la date en tant que type de données date pour commencer.

Également simplifié :

format('%s (%s)', p.abrev, p.prenome) AS determinador

Cela ne rendra pas la requête plus rapide, mais c'est beaucoup plus propre. Voir format() .

Avant toute chose, tous les conseils habituels pour l'optimisation des performances s'applique :

Si tout est correct, vous devriez voir des requêtes beaucoup plus rapides pour tous motifs.