web-dev-qa-db-fra.com

Postgres n'utilise pas d'index lorsque le scan d'index est une bien meilleure option

J'ai une simple requête pour joindre deux tables qui est vraiment lente. J'ai découvert que le plan de requête effectue une analyse séquentielle sur la grande table email_activities (~ 10m de lignes) alors que je pense que l'utilisation d'index faisant des boucles imbriquées sera en fait plus rapide.

J'ai réécrit la requête à l'aide d'une sous-requête pour tenter de forcer l'utilisation de l'index, puis j'ai remarqué quelque chose d'intéressant. Si vous regardez les deux plans de requête ci-dessous, vous verrez que lorsque je limite le jeu de résultats de la sous-requête à 43 k, le plan de requête utilise l'index sur les activités de messagerie tout en définissant la limite de la sous-requête à 44 k même, le plan de requête utilisera l'analyse séquentielle sur email_activities. L'un est clairement plus efficace que l'autre, mais Postgres ne semble pas s'en soucier.

Qu'est-ce qui pourrait provoquer cela? Y a-t-il une configuration quelque part qui force l'utilisation de la jointure par hachage si l'un des ensembles est supérieur à une certaine taille?

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 43000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=118261.50..118261.50 rows=1 width=4) (actual time=224.556..224.556 rows=1 loops=1)
   ->  Nested Loop  (cost=3699.03..118147.99 rows=227007 width=4) (actual time=32.586..209.076 rows=40789 loops=1)
         ->  HashAggregate  (cost=3698.94..3827.94 rows=43000 width=4) (actual time=32.572..47.276 rows=43000 loops=1)
               ->  Limit  (cost=0.09..3548.44 rows=43000 width=4) (actual time=0.017..22.547 rows=43000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.017..19.168 rows=43000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
         ->  Index Only Scan using index_email_activities_on_email_recipient_id on email_activities  (cost=0.09..2.64 rows=5 width=4) (actual time=0.003..0.003 rows=1 loops=43000)
               Index Cond: (email_recipient_id = email_recipients.id)
               Heap Fetches: 40789
 Total runtime: 224.675 ms

Et:

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 50000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=119306.25..119306.25 rows=1 width=4) (actual time=3050.612..3050.613 rows=1 loops=1)
   ->  Hash Semi Join  (cost=4451.08..119174.27 rows=263962 width=4) (actual time=1831.673..3038.683 rows=47935 loops=1)
         Hash Cond: (email_activities.email_recipient_id = email_recipients.id)
         ->  Seq Scan on email_activities  (cost=0.00..107490.96 rows=9359988 width=4) (actual time=0.003..751.988 rows=9360039 loops=1)
         ->  Hash  (cost=4276.08..4276.08 rows=50000 width=4) (actual time=34.058..34.058 rows=50000 loops=1)
               Buckets: 8192  Batches: 1  Memory Usage: 1758kB
               ->  Limit  (cost=0.09..4126.08 rows=50000 width=4) (actual time=0.016..27.302 rows=50000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.016..22.244 rows=50000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
 Total runtime: 3050.660 ms
  • Version: PostgreSQL 9.3.10 sur x86_64-unknown-linux-gnu, compilé par gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3, 64 bits
  • email_activities: ~ 10m de lignes
  • email_recipients: ~ 11 millions de lignes
16
Ryan Her

Scan d'index -> scan d'index bitmap -> scan séquentiel

Pour quelques lignes, il est avantageux d'exécuter une analyse d'index. Avec plus de lignes à renvoyer (pourcentage plus élevé de la table et en fonction de la distribution des données, des fréquences de valeurs et de la largeur des lignes), il devient plus probable de trouver plusieurs lignes sur une page de données. Ensuite, il est avantageux de passer à une analyse d'index bitmap. Une fois qu'un grand pourcentage de pages de données doit être visité de toute façon, il est moins coûteux d'exécuter une analyse séquentielle, de filtrer les lignes excédentaires et d'ignorer complètement les frais généraux pour les index.

Postgres passe à une analyse séquentielle, s'attendant à trouver rows=263962, C'est déjà 3% de la table entière. (Bien que seuls rows=47935 Soient réellement trouvés, voir ci-dessous.)

Plus dans cette réponse connexe:

Méfiez-vous de forcer les plans de requête

Vous ne pouvez pas forcer une certaine méthode de planification directement dans Postgres, mais vous pouvez rendre les méthodes autres extrêmement coûteuses à des fins de débogage. Voir Configuration de la méthode du planificateur dans le manuel.

SET enable_seqscan = off (Comme suggéré dans une autre réponse) fait cela pour des analyses séquentielles. Mais cela est destiné à des fins de débogage dans votre session uniquement. Utilisez pas utilisez-le comme paramètre général dans la production, sauf si vous savez exactement ce que vous faites. Cela peut forcer des plans de requête ridicules. Citant le manuel :

Ces paramètres de configuration fournissent une méthode grossière pour influencer les plans de requête choisis par l'optimiseur de requête. Si le plan par défaut choisi par l'optimiseur pour une requête particulière n'est pas optimal, une solution temporaire consiste à utiliser l'un de ces paramètres de configuration pour forcer l'optimiseur à choisissez un plan différent. De meilleures façons d'améliorer la qualité des plans choisis par l'optimiseur incluent l'ajustement manuel des constantes de coût de la raboteuse (voir Section 18.7.2 ), l'exécution de ANALYZE manuellement, augmenter la valeur du paramètre de configuration default_statistics_target et augmenter la quantité de statistiques collectées pour des colonnes spécifiques à l'aide de ALTER TABLE SET STATISTICS.

C'est déjà la plupart des conseils dont vous avez besoin.

Dans ce cas particulier, Postgres attend 5-6 fois plus de hits sur email_activities.email_recipient_id Que ce qui est réellement trouvé:

rows=227007 estimé contre actual ... rows=40789
estimé rows=263962 contre actual ... rows=47935

Si vous exécutez souvent cette requête, il sera avantageux que ANALYZE examine un échantillon plus grand pour des statistiques plus précises sur la colonne particulière. Votre table est grande (~ 10 millions de lignes), alors faites ceci:

ALTER TABLE email_activities ALTER COLUMN email_recipient_id
SET STATISTICS 3000;  -- max 10000, default 100

Alors ANALYZE email_activities;

Mesure de dernier recours

Dans très rare cas, vous pourriez avoir recours à forcer un index avec SET LOCAL enable_seqscan = off Dans une transaction distincte ou dans une fonction avec son propre environnement. Comme:

CREATE OR REPLACE FUNCTION f_count_dist_recipients(_email_campaign_id int, _limit int)
  RETURNS bigint AS
$func$
   SELECT COUNT(DISTINCT a.email_recipient_id)
   FROM   email_activities a
   WHERE  a.email_recipient_id IN (
      SELECT id
      FROM   email_recipients
      WHERE  email_campaign_id = $1
      LIMIT  $2)       -- or consider query below
$func$  LANGUAGE sql VOLATILE COST 100000 SET enable_seqscan = off;

Le paramètre s'applique uniquement à l'étendue locale de la fonction.

Attention: Ceci n'est qu'une preuve de concept. Même cette intervention manuelle beaucoup moins radicale pourrait vous mordre à long terme. Cardinalités, fréquences de valeurs, votre schéma, paramètres globaux Postgres, tout change avec le temps. Vous allez passer à une nouvelle version de Postgres. Le plan de requête que vous forcez maintenant peut devenir une très mauvaise idée plus tard.

Et généralement, ce n'est qu'une solution de contournement pour un problème avec votre configuration. Mieux vaut le trouver et le réparer.

Requête alternative

Des informations essentielles manquent dans la question, mais cette requête équivalente est probablement plus rapide et plus susceptible d'utiliser un index sur (email_recipient_id) - de plus en plus pour un LIMIT plus gros.

SELECT COUNT(*) AS ct
FROM  (
   SELECT id
   FROM   email_recipients
   WHERE  email_campaign_id = 1607
   LIMIT  43000
   ) r
WHERE  EXISTS (
   SELECT 1
   FROM   email_activities
   WHERE  email_recipient_id = r.id);
26
Erwin Brandstetter

Un scan séquentiel peut être plus efficace, même lorsqu'un index existe. Dans ce cas, les postgres semblent estimer les choses plutôt fausses. Une ANALYZE <TABLE> sur toutes les tables associées peut aider dans de tels cas. Si ce n'est pas le cas, vous pouvez définir la variable enable_seqscan à OFF, pour forcer les postgres à utiliser un index chaque fois que cela est techniquement possible, au détriment du fait que parfois un index-scan sera utilisé alors qu'un scan séquentiel fonctionnerait mieux.

2
Ctx