Mengapa rencana berbeda jika pertanyaannya sama secara logis?

19

Saya menulis dua fungsi untuk menjawab pertanyaan pekerjaan rumah pertama Hari 3 dari Seven Databases di Seven Weeks .

Buat prosedur tersimpan di mana Anda dapat memasukkan judul film atau nama aktor yang Anda sukai, dan itu akan mengembalikan lima saran teratas berdasarkan film yang aktornya bintangi atau film dengan genre serupa.

Upaya pertama saya benar tetapi lambat. Diperlukan waktu hingga 2000 ms untuk mengembalikan hasilnya.

CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
  RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (

  SELECT
    actors.name AS entity_term,
    movies.movie_id AS suggestion_id,
    movies.title AS suggestion_title,
    1 AS rank
  FROM actors
  INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
  INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)

  UNION ALL

  SELECT
    searches.title AS entity_term,
    suggestions.movie_id AS suggestion_id,
    suggestions.title AS suggestion_title,
    RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
  FROM movies AS searches
  INNER JOIN movies AS suggestions ON
    (searches.movie_id <> suggestions.movie_id) AND
    (cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
)
SELECT suggestion_id, suggestion_title
FROM suggestions
WHERE entity_term = query
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;

Upaya kedua saya benar dan cepat. Saya mengoptimalkannya dengan mendorong filter turun dari CTE ke setiap bagian serikat.

Saya menghapus baris ini dari kueri luar:

WHERE entity_term = query

Saya menambahkan baris ini ke permintaan dalam pertama:

WHERE actors.name = query

Saya menambahkan baris ini ke permintaan dalam kedua:

WHERE movies.title = query

Fungsi kedua membutuhkan sekitar 10 ms untuk mengembalikan hasil yang sama.

Tidak ada yang berbeda dalam database selain dari definisi fungsi.

Mengapa PostgreSQL menghasilkan paket berbeda untuk kedua pertanyaan yang secara logis setara ini?

The EXPLAIN ANALYZErencana fungsi pertama terlihat seperti ini:

                                                                                       Limit  (cost=7774.18..7774.19 rows=5 width=44) (actual time=1738.566..1738.567 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=332.56..7337.19 rows=19350 width=285) (actual time=7.113..1577.823 rows=383024 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=332.56..996.80 rows=11168 width=33) (actual time=7.113..22.258 rows=11168 loops=1)
                 ->  Hash Join  (cost=332.56..885.12 rows=11168 width=33) (actual time=7.110..19.850 rows=11168 loops=1)
                       Hash Cond: (movies_actors.movie_id = movies.movie_id)
                       ->  Hash Join  (cost=143.19..514.27 rows=11168 width=18) (actual time=4.326..11.938 rows=11168 loops=1)
                             Hash Cond: (movies_actors.actor_id = actors.actor_id)
                             ->  Seq Scan on movies_actors  (cost=0.00..161.68 rows=11168 width=8) (actual time=0.013..1.648 rows=11168 loops=1)
                             ->  Hash  (cost=80.86..80.86 rows=4986 width=18) (actual time=4.296..4.296 rows=4986 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 252kB
                                   ->  Seq Scan on actors  (cost=0.00..80.86 rows=4986 width=18) (actual time=0.009..1.681 rows=4986 loops=1)
                       ->  Hash  (cost=153.61..153.61 rows=2861 width=19) (actual time=2.768..2.768 rows=2861 loops=1)
                             Buckets: 1024  Batches: 1  Memory Usage: 146kB
                             ->  Seq Scan on movies  (cost=0.00..153.61 rows=2861 width=19) (actual time=0.003..1.197 rows=2861 loops=1)
           ->  Subquery Scan on "*SELECT* 2"  (cost=6074.48..6340.40 rows=8182 width=630) (actual time=1231.324..1528.188 rows=371856 loops=1)
                 ->  WindowAgg  (cost=6074.48..6258.58 rows=8182 width=630) (actual time=1231.324..1492.106 rows=371856 loops=1)
                       ->  Sort  (cost=6074.48..6094.94 rows=8182 width=630) (actual time=1231.307..1282.550 rows=371856 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: external sort  Disk: 21584kB
                             ->  Nested Loop  (cost=0.27..3246.72 rows=8182 width=630) (actual time=0.047..909.096 rows=371856 loops=1)
                                   ->  Seq Scan on movies searches  (cost=0.00..153.61 rows=2861 width=315) (actual time=0.003..0.676 rows=2861 loops=1)
                                   ->  Index Scan using movies_genres_cube on movies suggestions_1  (cost=0.27..1.05 rows=3 width=315) (actual time=0.016..0.277 rows=130 loops=2861)
                                         Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
   ->  Sort  (cost=436.99..437.23 rows=97 width=44) (actual time=1738.565..1738.566 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..435.38 rows=97 width=44) (actual time=1281.905..1738.531 rows=43 loops=1)
               Filter: (entity_term = 'Die Hard'::text)
               Rows Removed by Filter: 382981
 Total runtime: 1746.623 ms

The EXPLAIN ANALYZErencana query kedua terlihat seperti ini:

 Limit  (cost=43.74..43.76 rows=5 width=44) (actual time=1.231..1.234 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=4.86..43.58 rows=5 width=391) (actual time=1.029..1.141 rows=43 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=4.86..20.18 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                 ->  Nested Loop  (cost=4.86..20.16 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                       ->  Nested Loop  (cost=4.58..19.45 rows=2 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                             ->  Index Scan using actors_name on actors  (cost=0.28..8.30 rows=1 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                                   Index Cond: (name = 'Die Hard'::text)
                             ->  Bitmap Heap Scan on movies_actors  (cost=4.30..11.13 rows=2 width=8) (never executed)
                                   Recheck Cond: (actor_id = actors.actor_id)
                                   ->  Bitmap Index Scan on movies_actors_actor_id  (cost=0.00..4.30 rows=2 width=0) (never executed)
                                         Index Cond: (actor_id = actors.actor_id)
                       ->  Index Scan using movies_pkey on movies  (cost=0.28..0.35 rows=1 width=19) (never executed)
                             Index Cond: (movie_id = movies_actors.movie_id)
           ->  Subquery Scan on "*SELECT* 2"  (cost=23.31..23.40 rows=3 width=630) (actual time=0.982..1.081 rows=43 loops=1)
                 ->  WindowAgg  (cost=23.31..23.37 rows=3 width=630) (actual time=0.982..1.064 rows=43 loops=1)
                       ->  Sort  (cost=23.31..23.31 rows=3 width=630) (actual time=0.963..0.971 rows=43 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: quicksort  Memory: 28kB
                             ->  Nested Loop  (cost=4.58..23.28 rows=3 width=630) (actual time=0.808..0.916 rows=43 loops=1)
                                   ->  Index Scan using movies_title on movies searches  (cost=0.28..8.30 rows=1 width=315) (actual time=0.025..0.027 rows=1 loops=1)
                                         Index Cond: (title = 'Die Hard'::text)
                                   ->  Bitmap Heap Scan on movies suggestions_1  (cost=4.30..14.95 rows=3 width=315) (actual time=0.775..0.844 rows=43 loops=1)
                                         Recheck Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
                                         ->  Bitmap Index Scan on movies_genres_cube  (cost=0.00..4.29 rows=3 width=0) (actual time=0.750..0.750 rows=44 loops=1)
                                               Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
   ->  Sort  (cost=0.16..0.17 rows=5 width=44) (actual time=1.230..1.231 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..0.10 rows=5 width=44) (actual time=1.034..1.187 rows=43 loops=1)
 Total runtime: 1.410 ms
Iain Samuel McLean Elder
sumber

Jawaban:

21

Tidak ada pushdown predikat otomatis untuk CTE

PostgreSQL 9.3 tidak melakukan predikat pushdown untuk CTE.

Pengoptimal yang melakukan pushdown predikat dapat bergerak ke mana klausa ke dalam kueri. Tujuannya adalah untuk menyaring data yang tidak relevan sedini mungkin. Selama kueri baru setara secara logis, mesin masih mengambil semua data yang relevan, sehingga menghasilkan hasil yang benar, hanya lebih cepat.

Pengembang inti Tom Lane menyinggung kesulitan menentukan kesetaraan logis pada milis kinerja pgsql .

CTE juga diperlakukan sebagai pagar pengoptimalan; ini bukan batasan pengoptimal untuk menjaga semantik tetap waras ketika CTE berisi permintaan yang bisa ditulis.

Pengoptimal tidak membedakan CTE hanya baca dari yang dapat ditulis, jadi terlalu konservatif ketika mempertimbangkan rencana. Perlakuan 'pagar' menghentikan pengoptimal dari memindahkan klausa di mana di dalam CTE, meskipun kita dapat melihatnya aman untuk melakukannya.

Kami dapat menunggu tim PostgreSQL untuk meningkatkan optimasi CTE, tetapi untuk sekarang untuk mendapatkan kinerja yang baik Anda harus mengubah gaya penulisan Anda.

Menulis ulang untuk kinerja

Pertanyaannya sudah menunjukkan satu cara untuk mendapatkan rencana yang lebih baik. Menduplikasi kondisi filter pada dasarnya mengkode efek pushdown predikat.

Dalam kedua paket, hasil salinan mesin baris ke meja kerja sehingga dapat mengurutkannya. Semakin besar meja kerja, semakin lambat kueri.

Paket pertama menyalin semua baris di tabel dasar ke meja kerja dan memindai untuk menemukan hasilnya. Untuk membuat segalanya lebih lambat, mesin harus memindai seluruh meja kerja karena tidak memiliki indeks.

Itu jumlah yang konyol dari pekerjaan yang tidak perlu. Bunyinya semua data dalam tabel dasar dua kali untuk menemukan jawabannya, ketika hanya ada sekitar 5 baris yang cocok dari sekitar 19350 baris di tabel dasar.

Paket kedua menggunakan indeks untuk menemukan baris yang cocok dan hanya menyalinnya ke meja kerja. Indeks secara efektif memfilter data untuk kami.

Pada halaman 85 dari The Art of SQL, Stéphane Faroult mengingatkan kita tentang harapan pengguna.

Untuk tingkat yang sangat besar, pengguna akhir menyesuaikan kesabaran mereka dengan jumlah baris yang mereka harapkan: ketika mereka meminta satu jarum, mereka sedikit memperhatikan ukuran tumpukan jerami.

Paket kedua berskala dengan jarum, jadi lebih cenderung membuat pengguna Anda senang.

Menulis ulang untuk pemeliharaan

Kueri baru lebih sulit untuk dipertahankan karena Anda dapat memperkenalkan cacat dengan mengubah satu filter epxression tetapi tidak yang lain.

Bukankah lebih bagus jika kita bisa menulis semuanya hanya sekali dan masih mendapatkan kinerja yang baik?

Kita dapat. Pengoptimal melakukan pushdown predikat untuk subqeri.

Contoh yang lebih sederhana lebih mudah untuk dijelaskan.

CREATE TABLE a (c INT);

CREATE TABLE b (c INT);

CREATE INDEX a_c ON a(c);

CREATE INDEX b_c ON b(c);

INSERT INTO a SELECT 1 FROM generate_series(1, 1000000);

INSERT INTO b SELECT 2 FROM a;

INSERT INTO a SELECT 3;

Ini membuat dua tabel masing-masing dengan kolom yang diindeks. Bersama-sama mereka mengandung satu juta 1, satu juta 2, dan satu 3.

Anda dapat menemukan jarum 3menggunakan salah satu dari pertanyaan ini.

-- CTE
EXPLAIN ANALYZE
WITH cte AS (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
)
SELECT c FROM cte WHERE c = 3;

-- Subquery
EXPLAIN ANALYZE
SELECT c
FROM (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
) AS subquery
WHERE c = 3;

Rencana untuk CTE lambat. Mesin memindai tiga tabel dan membaca sekitar empat juta baris. Dibutuhkan hampir 1000 milidetik.

CTE Scan on cte  (cost=33275.00..78275.00 rows=10000 width=4) (actual time=471.412..943.225 rows=1 loops=1)
  Filter: (c = 3)
  Rows Removed by Filter: 2000000
  CTE cte
    ->  Append  (cost=0.00..33275.00 rows=2000000 width=4) (actual time=0.011..409.573 rows=2000001 loops=1)
          ->  Seq Scan on a  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.010..114.869 rows=1000001 loops=1)
          ->  Seq Scan on b  (cost=0.00..18850.00 rows=1000000 width=4) (actual time=5.530..104.674 rows=1000000 loops=1)
Total runtime: 948.594 ms

Rencana untuk subquery cepat. Mesin hanya mencari setiap indeks. Dibutuhkan kurang dari satu milidetik.

Append  (cost=0.42..8.88 rows=2 width=4) (actual time=0.021..0.038 rows=1 loops=1)
  ->  Index Only Scan using a_c on a  (cost=0.42..4.44 rows=1 width=4) (actual time=0.020..0.021 rows=1 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 1
  ->  Index Only Scan using b_c on b  (cost=0.42..4.44 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 0
Total runtime: 0.065 ms

Lihat SQLFiddle untuk versi interaktif.

Iain Samuel McLean Elder
sumber
0

Rencananya sama di Postgres 12

Pertanyaan yang diajukan tentang Postgres 9.3. Lima tahun kemudian, versi itu sudah usang, tetapi apa yang berubah?

PostgreSQL 12 sekarang menampilkan CTE seperti ini.

Diikutkan dengan kueri (ekspresi tabel umum)

Ekspresi tabel umum (alias WITHkueri) sekarang dapat secara otomatis dimasukkan dalam kueri jika a) tidak rekursif, b) tidak memiliki efek samping dan c) hanya direferensikan sekali di bagian akhir kueri. Ini menghapus "pagar pengoptimalan" yang telah ada sejak diperkenalkannya WITHklausa di PostgreSQL 8.4

Jika perlu, Anda bisa memaksa kueri WITH untuk terwujud menggunakan klausa MATERIALIS, misalnya

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
Iain Samuel McLean Elder
sumber