ทำไมแผนต่างกันถ้าแบบสอบถามมีเหตุผลเหมือนกัน?


19

ฉันเขียนสองฟังก์ชั่นที่จะตอบวันที่ 3 เป็นครั้งแรกที่บ้านของคำถามจากเซเว่นฐานข้อมูลในเจ็ดสัปดาห์

สร้างกระบวนงานที่เก็บไว้ซึ่งคุณสามารถป้อนชื่อภาพยนตร์หรือชื่อของนักแสดงที่คุณชอบและมันจะส่งคืนคำแนะนำห้าอันดับแรกตามภาพยนตร์ที่นักแสดงติดดาวหรือภาพยนตร์ประเภทเดียวกัน

ความพยายามครั้งแรกของฉันถูกต้อง แต่ช้า อาจใช้เวลานานถึง 2000 มิลลิวินาทีในการส่งคืนผลลัพธ์

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;

ความพยายามครั้งที่สองของฉันถูกต้องและรวดเร็ว ฉันปรับให้เหมาะสมโดยการกดตัวกรองลงจาก CTE ลงในส่วนต่าง ๆ ของสหภาพ

ฉันลบบรรทัดนี้ออกจากข้อความค้นหาด้านนอก:

WHERE entity_term = query

ฉันเพิ่มบรรทัดนี้ในการสืบค้นภายในครั้งแรก:

WHERE actors.name = query

ฉันเพิ่มบรรทัดนี้ไปยังข้อความค้นหาภายในที่สอง:

WHERE movies.title = query

ฟังก์ชั่นที่สองใช้เวลาประมาณ 10ms ในการส่งคืนผลลัพธ์เดียวกัน

ไม่มีสิ่งใดแตกต่างไปจากฐานข้อมูลนอกเหนือจากนิยามฟังก์ชัน

เหตุใด PostgreSQL จึงสร้างแผนที่แตกต่างกันสำหรับคำค้นหาที่มีเหตุผลสองข้อนี้

EXPLAIN ANALYZEแผนของฟังก์ชั่นแรกที่มีลักษณะเช่นนี้

                                                                                       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

EXPLAIN ANALYZEแผนของแบบสอบถามที่สองมีลักษณะเช่นนี้

 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

คำตอบ:


21

ไม่มีการเลื่อนภาคแสดงผลอัตโนมัติสำหรับ CTE

PostgreSQL 9.3 ไม่ได้ทำการคำนวณแบบขยายลงสำหรับ CTE

เครื่องมือเพิ่มประสิทธิภาพที่ทำให้การขยายลงของเพรดิเคตสามารถย้ายตำแหน่งในส่วนคำสั่งภายใน เป้าหมายคือการกรองข้อมูลที่ไม่เกี่ยวข้องเร็วที่สุดเท่าที่จะทำได้ ตราบใดที่แบบสอบถามใหม่นั้นมีเหตุผลเทียบเท่าเครื่องยนต์ยังคงดึงข้อมูลที่เกี่ยวข้องทั้งหมดดังนั้นสร้างผลลัพธ์ที่ถูกต้องรวดเร็วยิ่งขึ้นเท่านั้น

alludes นักพัฒนาหลักทอมเลนความยากลำบากของการกำหนดตรรกะความสมดุลในการpgsql ประสิทธิภาพรายชื่อผู้รับจดหมาย

CTEs จะถือว่าเป็นรั้วการเพิ่มประสิทธิภาพ นี่ไม่ใช่ข้อ จำกัด ของเครื่องมือเพิ่มประสิทธิภาพมากพอที่จะรักษาซีแมนติกซีแมนเมื่อ CTE มีคิวรีที่เขียนได้

เครื่องมือเพิ่มประสิทธิภาพไม่แยกความแตกต่าง CTE แบบอ่านอย่างเดียวจากแบบเขียนได้ดังนั้นจะมีความระมัดระวังมากเมื่อพิจารณาแผน การรักษา 'รั้ว' จะหยุดเครื่องมือเพิ่มประสิทธิภาพไม่ให้ย้ายส่วนคำสั่งภายใน CTE แม้ว่าเราจะเห็นว่าปลอดภัยแล้วก็ตาม

เราสามารถรอให้ทีม PostgreSQL เพื่อปรับปรุงการเพิ่มประสิทธิภาพ CTE แต่ตอนนี้เพื่อให้ได้ประสิทธิภาพที่ดีคุณต้องเปลี่ยนสไตล์การเขียนของคุณ

เขียนซ้ำเพื่อประสิทธิภาพ

คำถามแสดงให้เห็นถึงวิธีหนึ่งในการวางแผนที่ดีกว่า การทำซ้ำเงื่อนไขตัวกรองเป็นหลักฮาร์ดโค้ดผลกระทบของภาคขยายลง

ในทั้งสองแผนเอ็นจิ้นจะคัดลอกแถวผลลัพธ์ไปยังโต๊ะทำงานเพื่อให้สามารถเรียงลำดับได้ ยิ่ง worktable ยิ่งแบบสอบถามยิ่งทำงานช้า

แผนแรกคัดลอกแถวทั้งหมดในตารางฐานไปยังโต๊ะทำงานและสแกนเพื่อค้นหาผลลัพธ์ ในการทำให้สิ่งต่าง ๆ ช้าลงเอ็นจิ้นจะต้องสแกนโต๊ะทำงานทั้งหมดเนื่องจากไม่มีดัชนี

นั่นเป็นจำนวนงานไร้สาระที่ไร้สาระ มันจะอ่านข้อมูลทั้งหมดในตารางฐานสองครั้งเพื่อค้นหาคำตอบเมื่อมีเพียง 5 แถวที่ตรงกันโดยประมาณจากแถวที่ประมาณ 19350 ในตารางฐาน

แผนสองใช้ดัชนีเพื่อค้นหาแถวที่ตรงกันและคัดลอกเฉพาะแถวเหล่านั้นไปยังตารางงาน ดัชนีกรองข้อมูลของเราอย่างมีประสิทธิภาพ

ในหน้า 85ของ The Art of SQL, Stéphane Faroult เตือนเราถึงความคาดหวังของผู้ใช้

ในระดับที่ใหญ่มากผู้ใช้ปรับความอดทนของพวกเขาให้ตรงกับจำนวนแถวที่พวกเขาคาดหวัง: เมื่อพวกเขาขอเข็มหนึ่งเข็มพวกเขาให้ความสนใจเพียงเล็กน้อยกับขนาดของกองหญ้า

แผนสองปรับขนาดด้วยเข็มดังนั้นมีแนวโน้มที่จะทำให้ผู้ใช้ของคุณมีความสุข

เขียนซ้ำสำหรับการบำรุงรักษา

แบบสอบถามใหม่นั้นยากที่จะรักษาเพราะคุณสามารถแนะนำข้อบกพร่องได้ด้วยการเปลี่ยน epxression ของตัวกรองหนึ่งตัว แต่ไม่ใช่ตัวอื่น

มันจะไม่ดีถ้าเราสามารถเขียนทุกอย่างเพียงครั้งเดียวและยังได้รับประสิทธิภาพที่ดี?

เราทำได้ เครื่องมือเพิ่มประสิทธิภาพจะแสดงคำอธิบายแบบย่อลงสำหรับชุดย่อย

ตัวอย่างที่ง่ายกว่านั้นจะอธิบายได้ง่ายกว่า

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;

สิ่งนี้จะสร้างสองตารางแต่ละตารางด้วยคอลัมน์ที่จัดทำดัชนี ร่วมกันพวกเขามีเป็นล้าน1S, A ล้าน2s 3และหนึ่ง

คุณสามารถค้นหาเข็มได้3โดยใช้ข้อความค้นหาอย่างใดอย่างหนึ่งเหล่านี้

-- 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;

แผนสำหรับ CTE นั้นช้า เอ็นจิ้นสแกนสามตารางและอ่านประมาณสี่ล้านแถว ใช้เวลาเกือบ 1,000 มิลลิวินาที

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

แผนสำหรับแบบสอบถามย่อยนั้นรวดเร็ว เครื่องมือค้นหาแต่ละดัชนี ใช้เวลาน้อยกว่าหนึ่งมิลลิวินาที

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

ดูSQLFiddleสำหรับเวอร์ชันเชิงโต้ตอบ


0

แผนเหมือนกันใน Postgres 12

คำถามที่ถามเกี่ยวกับ Postgres 9.3 ห้าปีต่อมารุ่นนั้นล้าสมัย แต่มีอะไรเปลี่ยนแปลงบ้าง

PostgreSQL 12ตอนนี้ inlines CTE เช่นนี้

Inlined With query (นิพจน์ตารางทั่วไป)

นิพจน์ตารางทั่วไป ( WITHแบบสอบถามแบบaka ) สามารถถูกแทรกโดยอัตโนมัติในคิวรีหากพวกเขาก) ไม่เรียกซ้ำ, b) ไม่มีผลข้างเคียงใด ๆ และค) มีการอ้างอิงเพียงครั้งเดียวในส่วนหลังของแบบสอบถาม สิ่งนี้จะลบ "กรอบการปรับให้เหมาะสม" ที่มีอยู่นับตั้งแต่มีการเปิดตัวWITHประโยคใน PostgreSQL 8.4

หากจำเป็นคุณสามารถบังคับให้ข้อความค้นหา WITH เป็นรูปเป็นร่างโดยใช้คำสั่ง MATERIALIZED เช่น

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.