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 :
- La recherche de trigrammes devient beaucoup plus lente à mesure que la chaîne de recherche s'allonge
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 . Postgres est obligé de convertir cela en un LEFT JOIN
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
:
- Exemple de requête pour afficher l'erreur d'estimation de cardinalité dans PostgreSQL
- SQL INNER JOIN sur plusieurs tables égal à la syntaxe WHERE
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.