Warum ist der CTE viel schlechter als Inline-Unterabfragen?

11

Ich versuche besser zu verstehen, wie der Abfrageplaner in postgresql funktioniert.

Ich habe diese Frage:

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

Es läuft in weniger als 10 ms in meiner Datenbank mit ungefähr 500.000 Einträgen in der Benutzertabelle.

Dann dachte ich, um die doppelten Unterauswahlen zu vermeiden, könnte ich die Abfrage wie folgt als CTE umschreiben:

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;

Diese umgeschriebene Abfrage dauert jedoch ungefähr 1 Sekunde! Warum passiert das? Ich sehe in den Erklärungen, dass der Geometrieindex nicht verwendet wird, aber kann dafür etwas getan werden? Vielen Dank!

Eine andere Möglichkeit, die Abfrage zu schreiben, ist:

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;

Dies wird jedoch auch so langsam sein wie der CTE.

Wenn ich andererseits die me-Parameter extrahiere und statisch einfüge, ist die Abfrage wieder schnell:

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

Erklären Sie die erste (schnelle) Abfrage

 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)

Erklären Sie die zweite (langsame) Abfrage

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
quelle
3
Ich habe kürzlich darüber geschrieben; Siehe blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Derzeit gibt es jedoch einige DNS-Probleme, die die Erreichbarkeit dieser Site einschränken können. Versuchen Sie eine Unterabfrage FROManstelle des CTE-Begriffs, um die besten Ergebnisse zu erzielen .
Craig Ringer
Was ist, wenn Sie (select id, latest_location from users where id = 2)als Cte verwenden? Vielleicht ist es das *, das dieses Problem verursacht
cha
Ich hätte gedacht, dass Sie nach den engsten Nutzern des anderen Geschlechts suchen würden :)
cha
@cha Macht keinen Unterschied in der Geschwindigkeit, wenn nur Geschlecht und Ort im cte ausgewählt werden. (In meinem Fall möchte ich den Durchschnitt ähnlicher Benutzer nehmen, nur dass ich die Abfrage für die Frage vereinfacht habe)
viblo
@CraigRinger Ich glaube nicht, dass es der Optimierungszaun ist. Ich habe auch Ihren Vorschlag ausprobiert und es war auch langsam. Auf der anderen Seite ist es schnell, wenn ich die Parameter manuell extrahiere (und in meinem Fall eine echte Option, das Endergebnis ist sowieso eine Funktion).
Viblo

Antworten:

11

Versuche dies:

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;

Wenn ich mir den schnellen Plan ansehe, springt mir Folgendes heraus (fett gedruckt):

 Limit (Kosten = 5,69..20,11 Zeilen = 50 Breite = 36) (tatsächliche Zeit = 0,512..8,114 Zeilen = 50 Schleifen = 1)
   InitPlan 1 ( gibt $ 0 zurück )
     -> Index-Scan mit users_pkey für Benutzer users_1 (Kosten = 0,42..2,64 Zeilen = 1 Breite = 32) (tatsächliche Zeit = 0,032..0,033 Zeilen = 1 Schleifen = 1)
           Index Cond: (id = 2)
   InitPlan 2 ( gibt $ 1 zurück )
     -> Index-Scan mit users_pkey für Benutzer users_2 (Kosten = 0,42..2,64 Zeilen = 1 Breite = 4) (tatsächliche Zeit = 0,009..0,010 Zeilen = 1 Schleifen = 1)
           Index Cond: (id = 2)
   -> Index-Scan mit users_latest_location_gix für Benutzer (Kosten = 0,41..70796,51 Zeilen = 245470 Breite = 36) (tatsächliche Zeit = 0,509..8,100 Zeilen = 50 Schleifen = 1)
         Bestellen nach: (latest_location   $ 0 )
         Filter: (Geschlecht = $ 1 )
         Vom Filter entfernte Zeilen: 20
 Gesamtlaufzeit: 8,211 ms
(12 Zeilen)

In der langsamen Version wertet der Abfrageplaner den Gleichheitsoperator ein genderund den Geometrieoperator ein latest_locationim Kontext eines Joins aus , wobei der Wert von memit jeder Zeile variieren kann (obwohl nur 1 Zeile korrekt geschätzt wurde). In der schnellen Version werden die Werte von genderund latest_locationals Skalare behandelt, da sie von Inline-Unterabfragen ausgegeben werden. Dies teilt dem Abfrageplaner mit, dass jeweils nur ein Wert zu verarbeiten ist. Dies ist der gleiche Grund, warum Sie den schnellen Plan erhalten, wenn Sie die Literalwerte einfügen.

Noah Yetter
quelle
Ich denke, Sie können jetzt meaus der fromKlausel entfernen .
Jarius Hebzo