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.
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.
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.)
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.
Je rencontre beaucoup ce problème et découvre une astuce simple en 2 parties.
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))
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.