SELECT DISTINCT ON ON subquery menggunakan rencana yang tidak efisien

8

Saya punya meja progresses(berisi urutan ratusan ribu catatan saat ini):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

dan tampilan v_latest_progressespertanyaan yang paling baru progressoleh user_iddan lesson_id:

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

Seorang pengguna dapat memiliki banyak kemajuan untuk setiap pelajaran yang diberikan, tetapi kami sering ingin menanyakan satu set kemajuan yang baru dibuat untuk satu set pengguna atau pelajaran (atau kombinasi keduanya).

Tampilan v_latest_progressesmelakukan ini dengan baik dan bahkan performant ketika saya menentukan satu set user_ids:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

Namun jika saya mencoba melakukan kueri yang sama menggantikan set user_ids dengan subquery, itu menjadi sangat tidak efisien:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

Yang saya coba cari tahu adalah mengapa PostgreSQL ingin melakukan DISTINCTkueri pada seluruh progressestabel sebelum disaring oleh subquery pada contoh kedua.

Adakah yang punya saran tentang cara meningkatkan permintaan ini?

Harun
sumber

Jawaban:

11

Harun,

Dalam karya terbaru saya, saya telah mencari beberapa pertanyaan serupa dengan PostgreSQL. PostgreSQL hampir selalu cukup bagus untuk menghasilkan rencana permintaan yang tepat, tetapi tidak selalu sempurna.

Beberapa saran sederhana adalah memastikan menjalankan di ANALYZEatas progressesmeja Anda untuk memastikan bahwa Anda telah memperbarui statistik, tetapi ini tidak dijamin untuk memperbaiki masalah Anda!

Untuk alasan yang mungkin terlalu bertele-tele untuk posting ini, saya telah menemukan beberapa perilaku aneh dalam pengumpulan statistik ANALYZEdan perencana kueri yang mungkin perlu diselesaikan dalam jangka panjang. Dalam jangka pendek, triknya adalah menulis ulang kueri Anda untuk mencoba dan meretas rencana kueri yang Anda inginkan.

Tanpa memiliki akses ke data Anda untuk pengujian, saya akan membuat dua saran berikut yang mungkin.

1) Gunakan ARRAY()

PostgreSQL memperlakukan array dan set rekaman secara berbeda dalam perencana kueri. Terkadang Anda akan berakhir dengan rencana kueri yang identik. Dalam hal ini, seperti dalam banyak kasus saya, Anda tidak.

Dalam kueri asli Anda, Anda memiliki:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

Sebagai umpan pertama untuk mencoba memperbaikinya, cobalah

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

Perhatikan perubahan subquery dari INmenjadi =ANY(ARRAY()).

2) Gunakan CTE

Trik lain adalah memaksakan optimasi yang terpisah, jika saran pertama saya tidak berhasil. Saya tahu banyak orang menggunakan trik ini, karena pertanyaan dalam CTE dioptimalkan dan terwujud terpisah dari permintaan utama.

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

Pada dasarnya, dengan membuat CTE user_selectionmenggunakan WITHklausa, Anda meminta PostgreSQL untuk melakukan optimasi terpisah pada subquery

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

dan kemudian mewujudkan hasil itu. Saya kemudian, sekali lagi menggunakan =ANY(ARRAY())ekspresi untuk mencoba memanipulasi rencana secara manual.

Dalam kasus ini, Anda mungkin tidak dapat mempercayai hanya hasil EXPLAIN, karena sudah berpikir bahwa itu menemukan solusi yang paling murah. Pastikan untuk menjalankan EXPLAIN (ANALYZE,BUFFERS)...untuk mengetahui berapa biaya sebenarnya dalam hal waktu dan membaca halaman.

Chris
sumber
Ternyata, saran pertama Anda berhasil. Biaya untuk permintaan itu adalah 144.07..144.6, JALAN di bawah 70.000 yang saya dapatkan! Terima kasih banyak.
Aaron
1
Ha! Senang bisa membantu. Saya banyak berjuang melalui "peretasan rencana permintaan" ini; itu sedikit seni di atas sains.
Chris
Saya telah belajar trik kiri dan kanan selama bertahun-tahun untuk mendapatkan database untuk melakukan apa yang saya inginkan dan saya harus mengatakan ini adalah salah satu situasi asing yang pernah saya tangani. Ini benar-benar sebuah seni. Saya sangat menghargai penjelasan Anda yang matang!
Aaron