web-dev-qa-db-fra.com

Requête PostgreSQL très lente lors de l'ajout d'une sous-requête

J'ai une requête relativement simple sur une table avec 1,5 M de lignes:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE production:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Jusqu'ici tout va bien, rapide et utilise les index disponibles.
Maintenant, si je modifie un peu une requête, le résultat sera:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

Le EXPLAIN ANALYZE la sortie est:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Pas si rapide, et en utilisant seq scan ...

Bien sûr, la requête d'origine exécutée par l'application est un peu plus complexe et même plus lente, et bien sûr, l'original généré en hibernation n'est pas (SELECT 9762715), mais la lenteur est là même pour ça (SELECT 9762715)! La requête est générée par hibernate, il est donc très difficile de les changer, et certaines fonctionnalités ne sont pas disponibles (par exemple UNION n'est pas disponible, ce qui serait rapide).

Questions

  1. Pourquoi l'index ne peut-il pas être utilisé dans le deuxième cas? Comment pourraient-ils être utilisés?
  2. Puis-je améliorer les performances des requêtes d'une autre manière?

Réflexions supplémentaires

Il semble que nous pourrions utiliser le premier cas en faisant manuellement un SELECT, puis en mettant la liste résultante dans la requête. Même avec 5000 numéros dans la liste IN (), c'est quatre fois plus rapide que la deuxième solution. Cependant, il semble juste FAUX (aussi, cela pourrait être 100 fois plus rapide :)). Il est totalement incompréhensible que le planificateur de requêtes utilise une méthode complètement différente pour ces deux requêtes, donc je voudrais trouver une meilleure solution à ce problème.

9
P.Péter

Mon collègue a trouvé un moyen de modifier la requête afin qu'elle ait besoin d'une simple réécriture et fasse ce qu'elle doit faire, c'est-à-dire faire la sous-sélection en une seule étape, puis effectuer les autres opérations sur le résultat:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

L'analyse d'expliquer est maintenant:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Il semble que nous pouvons créer un analyseur simple qui recherche et réécrit toutes les sous-sélections de cette façon, et l'ajoutons à un hook de mise en veille prolongée pour manipuler la requête native.

6
P.Péter

Le cœur du problème devient évident ici:

Seq Scan sur publication (coût = 0,01..349652,84 lignes = 744661 largeur = 8) (temps réel = 2735,888..2841,393 lignes = 1 boucles = 1)

Postgres estime qu'il retournera 744661 lignes alors qu'en fait, il s'agit d'une seule ligne. Si Postgres ne sait pas mieux à quoi s'attendre de la requête, il ne peut pas mieux planifier. Nous aurions besoin de voir la requête réelle cachée derrière (SELECT 9762715) - et probablement aussi de connaître la définition de la table, les contraintes, les cardinalités et la distribution des données. De toute évidence, Postgres n'est pas en mesure de prédire comment quelques lignes seront retournées par lui. Il peut y avoir des façons de réécrire la requête, selon ce qu'elle est .

Si vous savez que la sous-requête ne peut jamais retourner plus de n lignes, vous pouvez simplement dire à Postgres en utilisant:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Si n est suffisamment petit, Postgres basculera vers les analyses d'index (bitmap). Cependant, cela ne fonctionne que pour le cas simple. Arrête de fonctionner lors de l'ajout d'une condition OR: le planificateur de requêtes ne peut actuellement pas y faire face.

J'utilise rarement IN (SELECT ...) pour commencer. En règle générale, il existe un meilleur moyen de l'implémenter, souvent avec une semi-jointure EXISTS. Parfois avec un (LEFT) JOIN (LATERAL) ...

La solution de contournement évidente serait d'utiliser UNION, mais vous l'avez exclu. Je ne peux pas en dire plus sans connaître la sous-requête réelle et d'autres détails pertinents.

5
Erwin Brandstetter

Réponse à une deuxième question: Oui, vous pouvez ajouter ORDER BY à votre sous-requête, ce qui aura un impact positif. Mais c'est similaire à la solution "EXISTS (sous-requête)" en termes de performances. Il existe une différence significative, même si la sous-requête aboutit à deux lignes.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
1
iki