Les requêtes en texte intégral sur cette base de données (stockage de tickets RT ( Request Tracker )) semblent prendre beaucoup de temps à s'exécuter. Le tableau des pièces jointes (contenant les données de texte intégral) est d'environ 15 Go.
Le schéma de la base de données est le suivant, c'est environ 2 millions de lignes:
rt4 = # \ d + pièces jointes Tableau "public.attachments" Colonne | Type | Modificateurs | Stockage | La description ----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + ------------- id | entier | non nul par défaut nextval ('attachments_id_seq' :: regclass) | ordinaire | transactionid | entier | non nul | ordinaire | parent | entier | non nul par défaut 0 | ordinaire | messageid | caractère variable (160) | | étendu | sujet | caractère variable (255) | | étendu | nom de fichier | caractère variable (255) | | étendu | type de contenu | caractère variable (80) | | étendu | codage de contenu | caractère variable (80) | | étendu | contenu | texte | | étendu | en-têtes | texte | | étendu | créateur | entier | non nul par défaut 0 | ordinaire | créé | horodatage sans fuseau horaire | | ordinaire | contentindex | tsvector | | étendu | Index: "attachments_pkey" CLÉ PRIMAIRE, btree (id) "attachments1" btree (parent) "attachments2" btree (transactionid) "attachments3" btree (parent, transactionid) Gin "contentindex_idx" (contentindex) Possède des OID: non
Je peux interroger la base de données d'elle-même très rapidement (<1s) avec une requête telle que:
select objectid
from attachments
join transactions on attachments.transactionid = transactions.id
where contentindex @@ to_tsquery('frobnicate');
Cependant, lorsque RT exécute une requête qui est censée effectuer une recherche d'index de texte intégral sur la même table, cela prend généralement des centaines de secondes. La sortie d'analyse de la requête est la suivante:
Requete
SELECT COUNT(DISTINCT main.id)
FROM Tickets main
JOIN Transactions Transactions_1 ON ( Transactions_1.ObjectType = 'RT::Ticket' )
AND ( Transactions_1.ObjectId = main.id )
JOIN Attachments Attachments_2 ON ( Attachments_2.TransactionId = Transactions_1.id )
WHERE (main.Status != 'deleted')
AND ( ( ( Attachments_2.ContentIndex @@ plainto_tsquery('frobnicate') ) ) )
AND (main.Type = 'ticket')
AND (main.EffectiveId = main.id);
EXPLAIN ANALYZE
production
PLAN DE DEMANDE -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------- Agrégat (coût = 51210.60..51210.61 lignes = 1 largeur = 4) (temps réel = 477778.806..477778.806 lignes = 1 boucles = 1) -> Boucle imbriquée (coût = 0,00..51210,57 lignes = 15 largeur = 4) (temps réel = 17943,986..477775,174 lignes = 4197 boucles = 1) -> Boucle imbriquée (coût = 0,00..40643,08 lignes = 6507 largeur = 8) (temps réel = 8,526..20610,380 lignes = 1714818 boucles = 1) -> Seq Scan sur les tickets principaux (coût = 0,00..9818,37 lignes = 598 largeur = 8) (temps réel = 0,008..256,042 lignes = 96990 boucles = 1) Filtre: ((((status) :: text 'supprimé' :: text) AND (id = effectiveid) AND ((type) :: text = 'ticket' :: text)) -> Index Scan en utilisant transactions1 sur transactions transactions_1 (coût = 0,00..51,36 lignes = 15 largeur = 8) (temps réel = 0,102..0,202 lignes = 18 boucles = 96990) Index Cond: (((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id)) -> Index Scan en utilisant les pièces jointes2 sur les pièces jointes attachments_2 (coût = 0,00..1,61 lignes = 1 largeur = 4) (temps réel = 0,266..0,266 lignes = 0 boucles = 1714818) Index Cond: (transactionid = transactions_1.id) Filtre: (contentindex @@ plainto_tsquery ('frobnicate' :: text)) Durée d'exécution totale: 477778,883 ms
Pour autant que je sache, le problème semble être qu'il n'utilise pas l'index créé sur le contentindex
champ ( contentindex_idx
), mais plutôt qu'il filtre un grand nombre de lignes correspondantes dans la table des pièces jointes. Le nombre de lignes dans la sortie d'explication semble également extrêmement inexact, même après une récente ANALYZE
: lignes estimées = 6507 lignes réelles = 1714818.
Je ne sais pas vraiment où aller ensuite avec ça.
Réponses:
Cela peut être amélioré de mille et une façons, alors cela devrait être une question de millisecondes .
Meilleures requêtes
Ceci est juste votre requête reformatée avec des alias et du bruit supprimé pour effacer le brouillard:
La plupart du problème avec votre requête réside dans les deux premières tables
tickets
ettransactions
, qui manquent dans la question. Je remplis de suppositions éclairées.t.status
,t.objecttype
ettr.objecttype
ne devrait probablement pas l'êtretext
, maisenum
ou peut-être une très petite valeur référençant une table de recherche.EXISTS
semi-jointureEn supposant que
tickets.id
c'est la clé primaire, cette forme réécrite devrait être beaucoup moins chère:Au lieu de multiplier les lignes avec deux jointures 1: n
count(DISTINCT id)
, utilisez uniquement uneEXISTS
semi-jointure pour réduire plusieurs correspondances à la fin , ce qui peut arrêter de chercher plus loin dès que la première correspondance est trouvée et en même temps obsolète la dernièreDISTINCT
étape. Par documentation:L'efficacité dépend du nombre de transactions par ticket et des pièces jointes par transaction.
Déterminer l'ordre des jointures avec
join_collapse_limit
Si vous savez que votre terme de recherche pour
attachments.contentindex
est très sélectif - plus sélectif que les autres conditions de la requête (ce qui est probablement le cas pour «frobniquer», mais pas pour «problème»), vous pouvez forcer la séquence de jointures. Le planificateur de requêtes peut difficilement juger de la sélectivité de certains mots, à l'exception des mots les plus courants. Par documentation:Utilisez
SET LOCAL
dans le but de le définir uniquement pour la transaction en cours.L'ordre des
WHERE
conditions est toujours sans importance. Seul l'ordre des jointures est pertinent ici.Ou utilisez un CTE comme @jjanes explique dans "Option 2". pour un effet similaire.
Index
Index B-tree
Prenez toutes les conditions
tickets
qui sont utilisées de manière identique avec la plupart des requêtes et créez un index partiel surtickets
:Si l'une des conditions est variable, supprimez-la de la
WHERE
condition et ajoutez plutôt la colonne comme colonne d'index.Un autre sur
transactions
:La troisième colonne sert uniquement à activer les analyses d'index uniquement.
De plus, puisque vous avez cet index composite avec deux colonnes entières sur
attachments
:Cet index supplémentaire est un déchet complet , supprimez-le:
Détails:
Indice GIN
Ajoutez
transactionid
à votre index GIN pour le rendre beaucoup plus efficace. Cela peut être une autre solution miracle , car il permet potentiellement des analyses d'index uniquement, éliminant complètement les visites à la grande table.Vous avez besoin de classes d'opérateur supplémentaires fournies par le module supplémentaire
btree_gin
. Instructions détaillées:4 octets d'une
integer
colonne ne rendent pas l'index beaucoup plus grand. De plus, heureusement pour vous, les index GIN sont différents des index B-tree dans un aspect crucial. Par documentation:Accentuation sur moi. Donc, vous avez juste besoin du seul indice GIN (gros et quelque peu coûteux).
Définition de table
Déplacez le
integer not null columns
vers l'avant. Cela a quelques effets positifs mineurs sur le stockage et les performances. Enregistre 4 à 8 octets par ligne dans ce cas.la source
Option 1
Le planificateur n'a aucune idée de la vraie nature de la relation entre EffectiveId et id, et pense donc probablement la clause:
va être beaucoup plus sélectif qu'il ne l'est réellement. Si c'est ce que je pense, EffectiveID est presque toujours égal à main.id, mais le planificateur ne le sait pas.
Une meilleure façon de stocker ce type de relation est généralement de définir la valeur NULL de EffectiveID pour signifier "effectivement la même chose que id", et d'y stocker quelque chose uniquement s'il y a une différence.
En supposant que vous ne vouliez pas réorganiser votre schéma, vous pouvez essayer de le contourner en réécrivant cette clause comme quelque chose comme:
Le planificateur peut supposer que l'
between
est moins sélective qu'une égalité, et cela pourrait suffire à le faire sortir de son piège actuel.Option 2
Une autre approche consiste à utiliser un CTE:
Cela oblige le planificateur à utiliser ContentIndex comme source de sélectivité. Une fois qu'il est obligé de le faire, les corrélations de colonnes trompeuses sur la table Tickets ne seront plus aussi attrayantes. Bien sûr, si quelqu'un recherche «problème» plutôt que «frobniquer», cela pourrait se retourner.
Option 3
Pour approfondir les estimations des mauvaises lignes, vous devez exécuter la requête ci-dessous dans toutes les permutations 2 ^ 3 = 8 des différentes clauses AND commentées. Cela vous aidera à déterminer d'où vient la mauvaise estimation.
la source