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

10

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;

La EXPLAIN ANALYZEsortie 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 ne l'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 modifier, et certaines fonctionnalités ne sont pas disponibles (par exemple, UNIONne sont pas disponibles, 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 effectuant manuellement un SELECT, puis en plaçant 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, cela 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.

P.Péter
la source
Pouvez-vous en quelque sorte réécrire votre code afin que la mise en veille prolongée génère un JOINau lieu du IN ()? En outre, a publicationété analysé récemment?
dezso
Oui, j'ai fait à la fois VACUUM ANALYZE et VACUUM FULL. Il n'y a eu aucun changement de performance. Quant au second, AFAIR, nous avons essayé cela et cela n'a pas affecté de manière significative les performances des requêtes.
P.Péter
1
Si Hibernate ne parvient pas à générer une requête appropriée, pourquoi ne pas simplement utiliser du SQL brut? C'est comme insister sur la traduction de Google alors que vous savez déjà mieux comment l'exprimer en anglais. Quant à votre question: cela dépend vraiment de la requête réelle cachée derrière (SELECT 9762715).
Erwin Brandstetter
Comme je l'ai mentionné ci-dessous, il est lent même si la requête interne l' est (SELECT 9762715) . À la question d'hibernation: cela pourrait être fait, mais nécessite une réécriture sérieuse du code, car nous avons des requêtes de critères d'hibernation définies par l'utilisateur qui sont traduites à la volée. Donc, essentiellement, nous modifierions Hibernate, ce qui est une énorme entreprise avec beaucoup d'effets secondaires possibles.
P.Péter

Réponses:

6

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

Scan Seq 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 renvoyer 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 ne peut prédire comment quelques lignes sont renvoyées par elle. 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 renvoyer plus de nlignes, 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 ORcondition: 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 EXISTSsemi-jointure. Parfois avec un ( LEFT) JOIN( LATERAL) ...

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

Erwin Brandstetter
la source
2
Il n'y a aucune requête cachée derrière (SELECT 9762715) ! Si je lance cette requête exacte que vous voyez ci-dessus. Bien sûr, la requête d'hibernation d'origine est un peu plus compliquée, mais j'ai (je pense) réussi à déterminer où le planificateur de requêtes s'égare, j'ai donc présenté cette partie de la requête. Cependant, les explications ci-dessus et les requêtes sont textuellement ctrl-cv.
P.Péter
Quant à la deuxième partie, la limite intérieure ne fonctionne pas: effectue EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;également un balayage séquentiel et dure également environ 3 secondes ...
P.Péter
@ P.Péter: Cela fonctionne pour moi dans mon test local avec une sous-requête réelle sur Postgres 9.4. Si ce que vous montrez est votre vraie requête, alors vous avez déjà votre solution: utilisez la première requête de votre question avec une constante au lieu d'une sous-requête.
Erwin Brandstetter
Eh bien, j'ai essayé aussi une sous - requête sur une nouvelle table de test: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. Et l'effet était toujours là pour les mêmes requêtes test: n'importe quelle sous-requête résultait d'un scan séquentiel ... J'ai essayé les versions 9.1 et 9.4. L'effet est le même.
P.Péter
1
@ P.Péter: J'ai refait le test et j'ai réalisé que j'avais testé sans la ORcondition. L'astuce avec LIMITne fonctionne que pour le cas le plus simple.
Erwin Brandstetter du
6

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.

P.Péter
la source
Cela semble amusant. N'est-il pas plus facile de simplement supprimer tous les SELECTs, comme vous l'avez fait dans votre première requête dans la question?
dezso
Bien sûr, je pourrais faire une approche en deux étapes: faire SELECTséparément, puis faire la sélection externe avec une liste statique après le IN. Cependant, cela est beaucoup plus lent (5 à 10 fois si la sous-requête a plus de quelques résultats), car vous avez des allers-retours réseau supplémentaires et vous avez un format postgres beaucoup de résultats, puis une analyse java de ces résultats (puis faire la même chose à l'envers). La solution ci-dessus fait de même sémantiquement, tout en laissant le processus à l'intérieur des postgres. Dans l'ensemble, cela semble actuellement être le moyen le plus rapide avec la plus petite modification dans notre cas.
P.Péter
Ah, je vois. Ce que je ne savais pas, c'est que vous pouvez obtenir plusieurs identifiants à la fois.
dezso
1

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;
iki
la source