web-dev-qa-db-fra.com

Requête lente sur une grande table avec GROUP BY et ORDER BY

J'ai une table avec 7,2 millions de tuples qui ressemble à ceci:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Maintenant, je veux sélectionner certaines valeurs mais la requête est incroyablement lente:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

La colonne hash est le hachage md5 de string et possède un index. Je pense donc que mon problème est que la table entière est triée par identifiant et non par hachage, il faut donc un certain temps pour la trier d'abord, puis la regrouper?

La table nostring ne contient qu'une liste de hachages que je ne veux pas avoir. Mais j'ai besoin des deux tables pour avoir toutes les valeurs. Ce n'est donc pas une option pour les supprimer.

info supplémentaire: aucune des colonnes ne peut être nulle (corrigé cela dans la définition de la table) et j'utilise postgresql 9.2.

14
reox

Le LEFT JOIN Dans réponse de @ dezso devrait être bon. Un index, cependant, ne sera guère utile (en soi), car la requête doit de toute façon lire la table entière - l'exception étant les analyses d'index uniquement dans Postgres 9.2+ et les conditions favorables, voir ci-dessous.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Exécutez EXPLAIN ANALYZE Sur la requête. Plusieurs fois pour exclure les effets d'encaissement et le bruit. Comparez les meilleurs résultats.

Créez un index multi-colonnes qui correspond à votre requête:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Attendre? Après avoir dit qu'un index n'aiderait pas? Eh bien, nous en avons besoin pour CLUSTER la table:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Relancez EXPLAIN ANALYZE. Plus rapide? Ça devrait être.

CLUSTER est une opération unique pour réécrire la table entière dans l'ordre de l'index utilisé. C'est aussi effectivement un VACUUM FULL. Si vous voulez être sûr, vous exécuteriez un pré-test avec VACUUM FULL Seul pour voir ce qui peut être attribué à cela.

Si votre table voit beaucoup d'opérations d'écriture, l'effet se dégradera avec le temps. Planifiez CLUSTER en dehors des heures d'ouverture pour restaurer l'effet. Le réglage fin dépend de votre cas d'utilisation exact. Le manuel sur CLUSTER.

CLUSTER est un outil plutôt grossier, a besoin d'un verrou exclusif sur la table. Si vous ne pouvez pas vous le permettre, pensez à pg_repack qui peut faire de même sans verrouillage exclusif. Plus dans cette réponse ultérieure:


Si le pourcentage de NULL valeurs dans la colonne method est élevé (plus de ~ 20 pour cent, selon la valeur réelle tailles de lignes), un index partiel devrait aider:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(Votre mise à jour ultérieure montre que vos colonnes sont NOT NULL, Donc sans objet.)

Si vous exécutez PostgreSQL 9.2 ou version ultérieure (comme @deszo a commenté ) les index présentés peuvent être utiles sans CLUSTER si le planificateur peut utiliser des analyses d'index uniquement . Applicable uniquement dans des conditions favorables: aucune opération d'écriture qui affecterait la carte de visibilité depuis le dernier VACUUM et toutes les colonnes de la requête doivent être couvertes par l'index. Les tables en lecture seule peuvent l'utiliser à tout moment, tandis que les tables fortement écrites sont limitées. Plus de détails dans le wiki Postgres.

L'index partiel mentionné ci-dessus pourrait être encore plus utile dans ce cas.

Si , d'autre part, il y a nonNULL valeurs dans la colonne method, vous devriez
1.) Définissez-le NOT NULL Et
2.) Utilisez count(*) au lieu de count(method), c'est légèrement plus rapide et fait de même en l'absence de valeurs NULL.

Si vous devez souvent appeler cette requête et que la table est en lecture seule, créez un MATERIALIZED VIEW .


Point fin exotique: votre table est nommée nostring, mais semble contenir des hachages. En excluant les hachages au lieu des chaînes, il est possible que vous excluez plus de chaînes que prévu. Extrêmement peu probable, mais possible.

18
Erwin Brandstetter

Bienvenue sur DBA.SE!

Vous pouvez essayer de reformuler votre requête comme suit:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

ou une autre possibilité:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN est un récepteur typique pour les performances car il est difficile d'utiliser un index avec lui.

Cela peut être encore amélioré avec des index. Un index sur nostring.hash semble utile. Mais d'abord: qu'obtenez-vous maintenant? (Il serait préférable de voir la sortie de EXPLAIN ANALYZE puisque les coûts eux-mêmes ne disent pas le temps nécessaire aux opérations.)

5
dezso

Puisque le hachage est un md5, vous pouvez probablement essayer de le convertir en nombre: vous pouvez le stocker sous forme de nombre, ou simplement créer un index fonctionnel qui calcule ce nombre dans une fonction immuable.

D'autres personnes ont déjà créé une fonction pl/pgsql qui convertit (en partie) une valeur md5 du texte en chaîne. Voir https://stackoverflow.com/questions/9809381/hashing-a-string-to-a-numeric-value-in-postgressql pour un exemple

Je crois que vous passez vraiment beaucoup de temps à comparer des chaînes lors de la numérisation de l'index. Si vous parvenez à stocker cette valeur sous forme de nombre, cela devrait être vraiment vraiment plus rapide.

1
eppesuig

Je rencontre beaucoup ce problème et découvre une astuce simple en 2 parties.

  1. Créer un index de sous-chaîne sur la valeur de hachage: (7 est généralement une bonne longueur)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Demandez à vos recherches/jointures d'inclure une correspondance de sous-chaîne, de sorte que le planificateur de requêtes est suggéré d'utiliser l'index:

    ancien: WHERE hash = :kwarg

    nouveau: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Vous devriez aussi avoir un index sur le hash brut.

le résultat (généralement) est que le planificateur consultera d'abord l'indice de sous-chaîne et éliminera la plupart des lignes. puis il fait correspondre le hachage complet de 32 caractères à l'index (ou table) correspondant. cette approche a fait chuter les requêtes de 800 ms à 4 pour moi.

0
Jonathan Vanasco