Pourquoi le CTE est-il bien pire que les sous-requêtes en ligne

11

J'essaie de mieux comprendre comment fonctionne le planificateur de requêtes dans postgresql.

J'ai cette requête:

select id from users 
    where id <> 2
    and gender = (select gender from users where id = 2)
    order by latest_location::geometry <-> (select latest_location from users where id = 2) ASC
    limit 50

Il fonctionne en moins de 10 ms sur ma base de données avec environ 500 000 entrées dans la table des utilisateurs.

Ensuite, j'ai pensé que pour éviter les sous-sélections en double, je pouvais réécrire la requête en CTE, comme ceci:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Cependant, cette requête réécrite s'exécute en environ 1 seconde! Pourquoi cela arrive-t-il? Je vois dans l'explique qu'il n'utilise pas l'index de géométrie, mais peut-on faire quelque chose pour cela? Merci!

Une autre façon d'écrire la requête est:

select u.id, u.popularity from users u, (select gender, latest_location from users where id = 2) as me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Cependant, cela sera également aussi lent que le CTE.

Si d'un autre côté j'extrais les paramètres me et les insère statiquement, la requête est à nouveau rapide:

select u.id, u.popularity from users u
    where u.gender = 'male'
    order by  u.latest_location::geometry <-> '0101000000A49DE61DA71C5A403D0AD7A370F54340'::geometry ASC
    limit 50;

Expliquer la première (rapide) requête

 Limit  (cost=5.69..20.11 rows=50 width=36) (actual time=0.512..8.114 rows=50 loops=1)
   InitPlan 1 (returns $0)
     ->  Index Scan using users_pkey on users users_1  (cost=0.42..2.64 rows=1 width=32) (actual time=0.032..0.033 rows=1 loops=1)
           Index Cond: (id = 2)
   InitPlan 2 (returns $1)
     ->  Index Scan using users_pkey on users users_2  (cost=0.42..2.64 rows=1 width=4) (actual time=0.009..0.010 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Index Scan using users_latest_location_gix on users  (cost=0.41..70796.51 rows=245470 width=36) (actual time=0.509..8.100 rows=50 loops=1)
         Order By: (latest_location <-> $0)
         Filter: (gender = $1)
         Rows Removed by Filter: 20
 Total runtime: 8.211 ms
(12 rows)

Expliquer la deuxième requête (lente)

Limit  (cost=62419.82..62419.95 rows=50 width=76) (actual time=1024.963..1024.970 rows=50 loops=1)
   CTE me
     ->  Index Scan using users_pkey on users  (cost=0.42..2.64 rows=1 width=221) (actual time=0.037..0.038 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Sort  (cost=62417.18..63030.86 rows=245470 width=76) (actual time=1024.959..1024.963 rows=50 loops=1)
         Sort Key: ((u.latest_location <-> me.latest_location))
         Sort Method: top-N heapsort  Memory: 28kB
         ->  Hash Join  (cost=0.03..54262.85 rows=245470 width=76) (actual time=0.122..938.131 rows=288646 loops=1)
               Hash Cond: (u.gender = me.gender)
               ->  Seq Scan on users u  (cost=0.00..49353.41 rows=490941 width=48) (actual time=0.021..465.025 rows=490994 loops=1)
               ->  Hash  (cost=0.02..0.02 rows=1 width=36) (actual time=0.054..0.054 rows=1 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  CTE Scan on me  (cost=0.00..0.02 rows=1 width=36) (actual time=0.047..0.049 rows=1 loops=1)
 Total runtime: 1025.096 ms
viblo
la source
3
J'ai écrit à ce sujet récemment; voir blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Bien qu'il existe actuellement certains problèmes DNS qui peuvent limiter l'accessibilité de ce site. Essayez une sous-requête au FROMlieu du terme CTE pour de meilleurs résultats.
Craig Ringer
que faire si vous l'utilisez (select id, latest_location from users where id = 2)comme cte? C'est peut-être le * qui cause ce problème
cha
J'aurais pensé que vous chercheriez les utilisateurs les plus proches du sexe opposé :)
cha
@cha Ne fait aucune différence de vitesse pour sélectionner simplement le sexe et l'emplacement dans le cte. (Dans mon cas, je veux prendre la moyenne des utilisateurs similaires, juste que j'ai simplifié la requête pour la question)
viblo
@CraigRinger Je ne pense pas que ce soit la clôture d'optimisation. J'ai également essayé votre suggestion et elle a également été lente. En revanche, si j'extrais les paramètres manuellement c'est rapide (et c'est une vraie option dans mon cas, le résultat final est quand même une fonction).
viblo

Réponses:

11

Essaye ça:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = (select gender from me)
    order by  u.latest_location::geometry <-> (select latest_location from me)::geometry ASC
    limit 50;

Quand je regarde le plan rapide, voici ce qui me saute aux yeux (en gras):

 Limite (coût = 5,69..20,11 lignes = 50 largeur = 36) (temps réel = 0,512..8,114 lignes = 50 boucles = 1)
   InitPlan 1 ( retourne 0 $ )
     -> Index Scan en utilisant users_pkey sur les utilisateurs users_1 (coût = 0,42..2,64 lignes = 1 largeur = 32) (temps réel = 0,032..0,033 lignes = 1 boucles = 1)
           Index Cond: (id = 2)
   InitPlan 2 ( retourne 1 $ )
     -> Index Scan en utilisant users_pkey sur les utilisateurs users_2 (coût = 0,42..2,64 lignes = 1 largeur = 4) (temps réel = 0,009..0,010 lignes = 1 boucles = 1)
           Index Cond: (id = 2)
   -> Analyse d'index en utilisant users_latest_location_gix sur les utilisateurs (coût = 0,41 à 70796,51 lignes = 245470 largeur = 36) (temps réel = 0,509 à 8 100 lignes = 50 boucles = 1)
         Trier par: (last_location   $ 0 )
         Filtre: (sexe = 1 $ )
         Lignes supprimées par filtre: 20
 Durée d'exécution totale: 8,211 ms
(12 rangées)

Dans la version lente, le planificateur de requêtes évalue l'opérateur d'égalité activé genderet l'opérateur de géométrie activé latest_locationdans le contexte d'une jointure , où la valeur de mepeut varier avec chaque ligne (même si elle n'a correctement estimé qu'une seule ligne). Dans la version rapide, les valeurs de genderet latest_locationsont traitées comme des scalaires car elles sont émises par des sous-requêtes en ligne, ce qui indique au planificateur de requêtes qu'il n'a qu'une seule valeur à traiter. C'est la même raison pour laquelle vous obtenez le plan rapide lorsque vous collez les valeurs littérales.

Noah Yetter
la source
Je pense que vous pouvez retirer mede l' fromarticle maintenant.
Jarius Hebzo