Mengapa CTE jauh lebih buruk daripada subqueries inline

11

Saya mencoba untuk lebih memahami cara kerja perencana kueri di postgresql.

Saya punya pertanyaan ini:

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

Ini berjalan dalam waktu kurang dari 10 ms di basis data saya dengan sekitar 500 ribu entri di tabel pengguna.

Kemudian saya berpikir bahwa untuk menghindari duplikat subselect saya bisa menulis ulang kueri sebagai CTE, seperti ini:

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;

Namun, kueri yang ditulis ulang ini beroperasi sekitar 1 detik! Mengapa ini terjadi? Saya melihat di menjelaskan bahwa itu tidak menggunakan indeks geometri, tetapi dapatkah sesuatu dilakukan untuk itu? Terima kasih!

Cara lain untuk menulis kueri adalah:

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;

Namun, ini juga akan selambat CTE.

Jika di sisi lain saya mengekstrak parameter saya dan menyisipkannya secara statis, pertanyaannya cepat lagi:

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

Jelaskan kueri (cepat) pertama

 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)

Jelaskan kueri kedua (lambat)

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
getar
sumber
3
Saya menulis tentang ini baru-baru ini; lihat blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Meskipun saat ini ada beberapa masalah DNS yang dapat membatasi jangkauan situs itu. Coba subquery FROMsebagai ganti istilah CTE untuk hasil terbaik.
Craig Ringer
bagaimana jika Anda menggunakan (select id, latest_location from users where id = 2)cte? Mungkin * yang menyebabkan masalah ini
cha
Saya akan berpikir bahwa Anda akan mencari pengguna terdekat dari lawan jenis :)
cha
@cha Membuat perbedaan dalam kecepatan untuk hanya memilih jenis kelamin dan lokasi di cte. (Dalam kasus saya, saya ingin mengambil rata-rata pengguna yang serupa, hanya saja saya menyederhanakan kueri untuk pertanyaan)
viblo
@CraigRinger Saya tidak berpikir ini pagar optimasi. Saya juga mencoba saran Anda dan juga lambat. Di sisi lain, jika saya mengekstrak parameter secara manual, ia cepat (dan ini merupakan pilihan nyata dalam kasus saya, hasil akhirnya adalah fungsi).
Viblo

Jawaban:

11

Coba ini:

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;

Ketika saya melihat rencana cepat inilah yang melompat keluar pada saya (dicetak tebal):

 Batas (biaya = 5,69..20,11 baris = 50 lebar = 36) (waktu aktual = 0,512..8,114 baris = 50 loop = 1)
   InitPlan 1 ( mengembalikan $ 0 )
     -> Pemindaian Indeks menggunakan users_pkey pada pengguna users_1 (biaya = 0.42..2.64 baris = 1 lebar = 32) (waktu aktual = 0.032..0.033 baris = 1 loop = 1)
           Indeks Cond: (id = 2)
   InitPlan 2 ( mengembalikan $ 1 )
     -> Pemindaian Indeks menggunakan users_pkey pada pengguna users_2 (biaya = 0,42..2.64 baris = 1 lebar = 4) (waktu aktual = 0.009..0.010 baris = 1 loop = 1)
           Indeks Cond: (id = 2)
   -> Pemindaian Indeks menggunakan users_latest_location_gix pada pengguna (biaya = 0,41..70796.51 baris = 245470 lebar = 36) (waktu aktual = 0,509..8,100 baris = 50 loop = 1)
         Pesan Menurut: (latest_location   $ 0 )
         Saring: (jenis kelamin = $ 1 )
         Baris Dihapus oleh Filter: 20
 Total runtime: 8.211 ms
(12 baris)

Dalam versi lambat, perencana kueri sedang mengevaluasi operator kesetaraan aktif genderdan operator geometri aktif latest_locationdalam konteks gabungan , di mana nilai dari medapat bervariasi dengan setiap baris (meskipun dengan benar hanya memperkirakan 1 baris). Dalam versi cepat nilai-nilai genderdan latest_locationdiperlakukan sebagai skalar karena mereka dipancarkan oleh subqueries inline, yang memberi tahu perencana kueri itu hanya memiliki satu nilai masing-masing untuk menangani. Ini adalah alasan yang sama mengapa Anda mendapatkan paket cepat ketika Anda menempelkan nilai literal.

Noah Yetter
sumber
Saya pikir Anda dapat menghapus medari fromklausa sekarang.
Jarius Hebzo