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

14

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 quelques 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 hashcolonne est le hachage md5 stringet 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?

Le tableau nostringne 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.

reox
la source
1
Fournissez toujours la version de PostgreSQL que vous utilisez. Quel est le pourcentage de NULLvaleurs dans la colonne method? Y a-t-il des doublons string?
Erwin Brandstetter

Réponses:

18

La réponse deLEFT JOIN in @ dezso devrait être bonne. 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 ANALYZEsur 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);

Attendez? Après avoir dit qu'un index ne serait pas utile? Eh bien, nous en avons besoin CLUSTER:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

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

CLUSTERest une opération ponctuelle pour réécrire la table entière dans l'ordre de l'index utilisé. C'est aussi effectivement un VACUUM FULL. Si vous voulez en être sûr, vous effectuerez un pré-test avec VACUUM FULLseul 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 CLUSTERen dehors des heures de travail pour restaurer l'effet. Le réglage fin dépend de votre cas d'utilisation exact. Le manuel sur CLUSTER.

CLUSTERest un outil assez grossier, a besoin d'un verrou exclusif sur la table. Si vous ne pouvez pas vous le permettre, réfléchissez à celui pg_repackqui peut faire de même sans verrouillage exclusif. Plus dans cette réponse ultérieure:


Si le pourcentage de NULLvaleurs dans la colonne methodest élevé (plus de ~ 20%, selon la taille réelle des 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 le sont NOT NULL, donc sans objet.)

Si vous exécutez PostgreSQL 9.2 ou version ultérieure (comme l' a commenté @deszo ), les index présentés peuvent être utiles sans CLUSTERque le planificateur puisse utiliser des analyses d'index uniquement . Applicable uniquement dans des conditions favorables: aucune opération d'écriture qui affecterait la carte de visibilité car la dernière VACUUMcolonne 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 n'y a pas de NULL valeurs dans la colonne method, vous devez
1.) la définir NOT NULLet
2.) utiliser à la count(*)place de count(method), c'est légèrement plus rapide et fait de même en l'absence de NULLvaleurs.

Si vous devez appeler cette requête souvent 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 improbable, mais possible.

Erwin Brandstetter
la source
avec le cluster, c'est beaucoup plus rapide. il faut encore environ 5 minutes pour la requête, mais c'est bien mieux que de l'exécuter toute la nuit: D
reox
@reox: Depuis que vous exécutez la version 9.2: Avez-vous testé avec l'index uniquement, avant le clustering? Ce serait intéressant si vous voyiez une différence. (Vous ne pouvez pas reproduire la différence après la mise en cluster.) De plus (et ce serait bon marché), EXPLAIN affiche-t-il une analyse d'index ou une analyse complète de la table maintenant?
Erwin Brandstetter
5

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 puits typique de performances car il est difficile d'utiliser un index avec lui.

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

dezso
la source
un index est déjà créé sur nostring.hash, mais je pense que les postgres ne l'utilisent pas à cause d'un trop grand nombre de tuples ... quand j'explicite désactiver le balayage de séquence, il utilise l'index. si j'utilise la jointure gauche j'obtiens un coût de 32 millions, donc c'est mieux ... mais j'essaie de l'optimiser davantage ...
reox
3
Le coût est uniquement pour le planificateur de pouvoir choisir un plan suffisamment bon. Les temps réels sont généralement en corrélation avec lui, mais pas nécessairement. Donc, si vous voulez en être sûr, utilisez EXPLAIN ANALYZE.
dezso
1

Puisque le hachage est un md5, vous pouvez probablement essayer de le convertir en nombre: vous pouvez le stocker comme un 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 /programming/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.

eppesuig
la source
1
Je doute que cette conversion accélère les choses. Toutes les requêtes ici utilisent l'égalité pour la comparaison. Calculer des représentations numériques puis vérifier l'égalité ne me promet pas de gros gains.
dezso
2
Je pense que je stockerais md5 en tant que bytea plutôt qu'un nombre pour une meilleure efficacité de l'espace: sqlfiddle.com/#!12/d41d8/252
Jack dit d'essayer topanswers.xyz
Bienvenue également sur dba.se!
Jack dit d'essayer topanswers.xyz
@JackDouglas: Commentaire intéressant! 16 octets par md5 au lieu de 32, c'est assez pour les grandes tables.
Erwin Brandstetter
0

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:

    vieux: WHERE hash = :kwarg

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

Vous devriez également avoir un index sur le brut hash.

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.

Jonathan Vanasco
la source