ทำไม LEFT JOIN นี้ถึงทำงานแย่กว่า LEAT JOIN LATERAL มากนัก?


13

ฉันมีตารางต่อไปนี้ (นำมาจากฐานข้อมูล Sakila):

  • film: film_id คือ pkey
  • นักแสดง: actor_id คือกุญแจ
  • film_actor: film_id และ actor_id เป็น fkeys สำหรับภาพยนตร์ / นักแสดง

ฉันกำลังเลือกภาพยนตร์เฉพาะเรื่อง สำหรับหนังเรื่องนี้ฉันต้องการให้นักแสดงทุกคนเข้าร่วมในภาพยนตร์เรื่องนี้ด้วย ฉันมีสองคำสั่งสำหรับการนี้: หนึ่งที่มีและเป็นหนึ่งเดียวกับLEFT JOINLEFT JOIN LATERAL

select film.film_id, film.title, a.actors
from   film
left join
  (         
       select     film_actor.film_id, array_agg(first_name) as actors
       from       actor
       inner join film_actor using(actor_id)
       group by   film_actor.film_id
  ) as a
on       a.film_id = film.film_id
where    film.title = 'ACADEMY DINOSAUR'
order by film.title;

select film.film_id, film.title, a.actors
from   film
left join lateral
  (
       select     array_agg(first_name) as actors
       from       actor
       inner join film_actor using(actor_id)
       where      film_actor.film_id = film.film_id
  ) as a
on       true
where    film.title = 'ACADEMY DINOSAUR'
order by film.title;

เมื่อเปรียบเทียบแผนแบบสอบถามคิวรีแรกจะทำงานได้แย่กว่ามาก (20x) กว่าวินาที:

 Merge Left Join  (cost=507.20..573.11 rows=1 width=51) (actual time=15.087..15.089 rows=1 loops=1)
   Merge Cond: (film.film_id = film_actor.film_id)
   ->  Sort  (cost=8.30..8.31 rows=1 width=19) (actual time=0.075..0.075 rows=1 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.044..0.058 rows=1 loops=1)
           Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
   ->  GroupAggregate  (cost=498.90..552.33 rows=997 width=34) (actual time=15.004..15.004 rows=1 loops=1)
     Group Key: film_actor.film_id
     ->  Sort  (cost=498.90..512.55 rows=5462 width=8) (actual time=14.934..14.937 rows=11 loops=1)
           Sort Key: film_actor.film_id
           Sort Method: quicksort  Memory: 449kB
           ->  Hash Join  (cost=6.50..159.84 rows=5462 width=8) (actual time=0.355..8.359 rows=5462 loops=1)
             Hash Cond: (film_actor.actor_id = actor.actor_id)
             ->  Seq Scan on film_actor  (cost=0.00..84.62 rows=5462 width=4) (actual time=0.035..2.205 rows=5462 loops=1)
             ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.303..0.303 rows=200 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on actor  (cost=0.00..4.00 rows=200 width=10) (actual time=0.027..0.143 rows=200 loops=1)
 Planning time: 1.495 ms
 Execution time: 15.426 ms

 Nested Loop Left Join  (cost=25.11..33.16 rows=1 width=51) (actual time=0.849..0.854 rows=1 loops=1)
   ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.045..0.048 rows=1 loops=1)
     Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
   ->  Aggregate  (cost=24.84..24.85 rows=1 width=32) (actual time=0.797..0.797 rows=1 loops=1)
     ->  Hash Join  (cost=10.82..24.82 rows=5 width=6) (actual time=0.672..0.764 rows=10 loops=1)
           Hash Cond: (film_actor.actor_id = actor.actor_id)
           ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=2) (actual time=0.072..0.150 rows=10 loops=1)
             Recheck Cond: (film_id = film.film_id)
             Heap Blocks: exact=10
             ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.041..0.041 rows=10 loops=1)
               Index Cond: (film_id = film.film_id)
           ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.561..0.561 rows=200 loops=1)
             Buckets: 1024  Batches: 1  Memory Usage: 17kB
             ->  Seq Scan on actor  (cost=0.00..4.00 rows=200 width=10) (actual time=0.039..0.275 rows=200 loops=1)
 Planning time: 1.722 ms
 Execution time: 1.087 ms

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

ความคิดของฉัน: ในLEFT JOINแบบสอบถามแรกดูเหมือนว่าแบบสอบถามย่อยจะถูกดำเนินการสำหรับภาพยนตร์ทุกเรื่องในฐานข้อมูลโดยไม่คำนึงถึงการกรองในแบบสอบถามภายนอกที่เราสนใจเพียงหนึ่งเรื่อง เหตุใดผู้วางแผนไม่สามารถมีความรู้นั้นในแบบสอบถามย่อยได้?

ในLEFT JOIN LATERALแบบสอบถามเราจะ 'ดัน' ที่กรองลงมากขึ้นหรือน้อยลง ดังนั้นปัญหาที่เรามีในข้อความค้นหาแรกจึงไม่ปรากฏที่นี่ดังนั้นประสิทธิภาพที่ดีขึ้น

ฉันเดาว่าฉันกำลังมองหากฎของหัวแม่มือ, ภูมิปัญญาทั่วไป, ... ดังนั้นเวทย์มนตร์นักวางแผนนี้จึงกลายเป็นธรรมชาติที่สอง - ถ้ามันเหมาะสม

อัปเดต (1)

การเขียนใหม่LEFT JOINดังต่อไปนี้ยังให้ประสิทธิภาพที่ดีขึ้น (ดีกว่าเล็กน้อยLEFT JOIN LATERAL):

select film.film_id, film.title, array_agg(a.first_name) as actors
from   film
left join
  (         
       select     film_actor.film_id, actor.first_name
       from       actor
       inner join film_actor using(actor_id)
  ) as a
on       a.film_id = film.film_id
where    film.title = 'ACADEMY DINOSAUR'
group by film.film_id
order by film.title;

 GroupAggregate  (cost=29.44..29.49 rows=1 width=51) (actual time=0.470..0.471 rows=1 loops=1)
   Group Key: film.film_id
   ->  Sort  (cost=29.44..29.45 rows=5 width=25) (actual time=0.428..0.430 rows=10 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Nested Loop Left Join  (cost=4.74..29.38 rows=5 width=25) (actual time=0.149..0.386 rows=10 loops=1)
           ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.056..0.057 rows=1 loops=1)
             Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
           ->  Nested Loop  (cost=4.47..19.09 rows=200 width=8) (actual time=0.087..0.316 rows=10 loops=1)
             ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=4) (actual time=0.052..0.089 rows=10 loops=1)
               Recheck Cond: (film_id = film.film_id)
               Heap Blocks: exact=10
               ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.035..0.035 rows=10 loops=1)
                 Index Cond: (film_id = film.film_id)
             ->  Index Scan using actor_pkey on actor  (cost=0.14..0.17 rows=1 width=10) (actual time=0.011..0.011 rows=1 loops=10)
               Index Cond: (actor_id = film_actor.actor_id)
 Planning time: 1.833 ms
 Execution time: 0.706 ms

เราจะให้เหตุผลเกี่ยวกับเรื่องนี้ได้อย่างไร

อัปเดต (2)

ฉันยังคงมีการทดลองบางอย่างและฉันคิดว่ากฎที่น่าสนใจของหัวแม่มือคือ: ใช้ฟังก์ชันการรวมสูง / ปลายที่เป็นไปได้ ข้อความค้นหาในการอัปเดต (1) อาจทำงานได้ดีขึ้นเนื่องจากเรากำลังรวบรวมในแบบสอบถามด้านนอกไม่อยู่ในแบบสอบถามภายใน

ดูเหมือนว่าจะมีผลบังคับใช้ถ้าเราเขียนLEFT JOIN LATERALข้างต้นดังต่อไปนี้:

select film.film_id, film.title, array_agg(a.first_name) as actors
from   film
left join lateral
  (
       select     actor.first_name
       from       actor
       inner join film_actor using(actor_id)
       where      film_actor.film_id = film.film_id
  ) as a
on       true
where    film.title = 'ACADEMY DINOSAUR'
group by film.film_id
order by film.title;

 GroupAggregate  (cost=29.44..29.49 rows=1 width=51) (actual time=0.088..0.088 rows=1 loops=1)
   Group Key: film.film_id
   ->  Sort  (cost=29.44..29.45 rows=5 width=25) (actual time=0.076..0.077 rows=10 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Nested Loop Left Join  (cost=4.74..29.38 rows=5 width=25) (actual time=0.031..0.066 rows=10 loops=1)
           ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.010..0.010 rows=1 loops=1)
             Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
           ->  Nested Loop  (cost=4.47..19.09 rows=200 width=8) (actual time=0.019..0.052 rows=10 loops=1)
             ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=4) (actual time=0.013..0.024 rows=10 loops=1)
               Recheck Cond: (film_id = film.film_id)
               Heap Blocks: exact=10
               ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.007..0.007 rows=10 loops=1)
                 Index Cond: (film_id = film.film_id)
             ->  Index Scan using actor_pkey on actor  (cost=0.14..0.17 rows=1 width=10) (actual time=0.002..0.002 rows=1 loops=10)
               Index Cond: (actor_id = film_actor.actor_id)
 Planning time: 0.440 ms
 Execution time: 0.136 ms

ที่นี่เราขยับarray_agg()ขึ้นไป LEFT JOIN LATERALที่คุณสามารถดูแผนนี้ยังเป็นที่ดีขึ้นกว่าเดิม

ที่กล่าวว่าฉันไม่แน่ใจว่ากฎง่ายๆนี้ ( ใช้ฟังก์ชั่นรวมสูง / ช้าที่สุดเท่าที่เป็นไปได้ ) เป็นจริงในกรณีอื่น ๆ

ข้อมูลเพิ่มเติม

ซอ: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb

รุ่น: PostgreSQL 10.4 บน x86_64-pc-linux-musl รวบรวมโดย gcc (อัลไพน์ 6.4.0) 6.4.0, 64-bit

สภาพแวดล้อม: นักเทียบท่า: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila. โปรดทราบว่าภาพบนฮับ Docker นั้นล้าสมัยดังนั้นฉันจึงสร้างบิลด์ก่อน: build -t frantiseks/postgres-sakilaหลังจากทำการโคลนที่เก็บ git

คำจำกัดความของตาราง:

ฟิล์ม

 film_id              | integer                     | not null default nextval('film_film_id_seq'::regclass)
 title                | character varying(255)      | not null

 Indexes:
    "film_pkey" PRIMARY KEY, btree (film_id)
    "idx_title" btree (title)

 Referenced by:
    TABLE "film_actor" CONSTRAINT "film_actor_film_id_fkey" FOREIGN KEY (film_id) REFERENCES film(film_id) ON UPDATE CASCADE ON DELETE RESTRICT

นักแสดงชาย

 actor_id    | integer                     | not null default nextval('actor_actor_id_seq'::regclass)
 first_name  | character varying(45)       | not null

 Indexes:
    "actor_pkey" PRIMARY KEY, btree (actor_id)

 Referenced by:
    TABLE "film_actor" CONSTRAINT "film_actor_actor_id_fkey" FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ON UPDATE CASCADE ON DELETE RESTRICT

film_actor

 actor_id    | smallint                    | not null
 film_id     | smallint                    | not null

 Indexes:
    "film_actor_pkey" PRIMARY KEY, btree (actor_id, film_id)
    "idx_fk_film_id" btree (film_id)
 Foreign-key constraints:
    "film_actor_actor_id_fkey" FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ON UPDATE CASCADE ON DELETE RESTRICT
    "film_actor_film_id_fkey" FOREIGN KEY (film_id) REFERENCES film(film_id) ON UPDATE CASCADE ON DELETE RESTRICT

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

select count(*) from film: 1000
select count(*) from actor: 200
select avg(a) from (select film_id, count(actor_id) a from film_actor group by film_id) a: 5.47

1
อีกอย่างหนึ่ง: ข้อมูลสำคัญทั้งหมดควรเป็นคำถาม (รวมถึงลิงค์ซอของคุณ) ไม่มีใครต้องการอ่านความคิดเห็นทั้งหมดในภายหลัง (หรือพวกเขาจะถูกลบโดยผู้ดูแลที่มีความสามารถอยู่ดี)
Erwin Brandstetter

ซอถูกเพิ่มเข้าไปในคำถาม!
Jelly Orns

คำตอบ:


7

ทดสอบการตั้งค่า

การตั้งค่าดั้งเดิมของคุณในซอออกจากห้องเพื่อการปรับปรุง ฉันขอเก็บการตั้งค่าของคุณด้วยเหตุผลบางอย่าง

  • คุณมีดัชนีเหล่านี้ในfilm_actor:

    "film_actor_pkey" PRIMARY KEY, btree (actor_id, film_id)  
    "idx_fk_film_id" btree (film_id)
    

    ซึ่งมีประโยชน์อยู่แล้ว แต่การที่จะสนับสนุนที่ดีที่สุดแบบสอบถามโดยเฉพาะอย่างยิ่งของคุณคุณจะมีดัชนีหลายคอลัมน์ใน(film_id, actor_id)คอลัมน์ในลำดับนี้ โซลูชันที่ใช้ได้จริง: แทนที่idx_fk_film_idด้วยดัชนีบน(film_id, actor_id)- หรือสร้าง PK บน(film_id, actor_id)สำหรับจุดประสงค์ของการทดสอบนี้เหมือนกับที่ฉันทำด้านล่าง ดู:

    ในแบบอ่านอย่างเดียว (หรือส่วนใหญ่หรือโดยทั่วไปเมื่อ VACUUM สามารถติดตามกิจกรรมการเขียนได้) นอกจากนี้ยังช่วยให้มีการเปิดดัชนี(title, film_id)เพื่ออนุญาตการสแกนดัชนีเท่านั้น กรณีทดสอบของฉันได้รับการปรับแต่งอย่างเหมาะสมเพื่อประสิทธิภาพการอ่าน

  • พิมพ์ไม่ตรงกันระหว่างfilm.film_id( integer) และfilm_actor.film_id( smallint) ในขณะที่ใช้งานได้ทำให้แบบสอบถามช้าลงและสามารถนำไปสู่ภาวะแทรกซ้อนต่าง ทำให้ข้อ จำกัด ของ FK แพงกว่าด้วย ไม่เคยทำเช่นนี้หากสามารถหลีกเลี่ยงได้ หากคุณไม่แน่ใจให้เลือกมากกว่าinteger smallintในขณะที่smallint สามารถบันทึก 2 ไบต์ต่อข้อมูล (มักจะบริโภคโดยการจัดตำแหน่ง padding) integerมีภาวะแทรกซ้อนมากกว่าด้วย

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

ไม่เกี่ยวข้องกับการทดสอบนี้:

  • ลำดับยืนฟรีพร้อมค่าเริ่มต้นของคอลัมน์แทนคอลัมน์ที่ง่ายกว่าและเชื่อถือได้มากกว่าserial(หรือIDENTITY) อย่า

  • timestamp without timestampโดยทั่วไปไม่น่าเชื่อถือสำหรับคอลัมน์เช่นlast_updateนั้น ใช้timestamptzแทน และโปรดทราบว่าค่าเริ่มต้นของคอลัมน์จะไม่ครอบคลุม "การอัพเดทล่าสุด" ซึ่งเป็นการพูดอย่างเคร่งครัด

  • ตัวดัดแปลงความยาวในcharacter varying(255)บ่งชี้ว่ากรณีทดสอบไม่ได้มีไว้สำหรับ Postgres ที่จะเริ่มต้นด้วยเนื่องจากความยาวคี่จะไม่มีจุดหมายที่นี่ (หรือผู้เขียนคือ clueless)

พิจารณากรณีทดสอบการตรวจสอบในซอ:

db <> fiddle here - สร้างบนซอของคุณ, ปรับให้เหมาะสมและเพิ่มข้อความค้นหา

ที่เกี่ยวข้อง:

การตั้งค่าการทดสอบที่มี 1,000 เรื่องและนักแสดง 200 คนมีความถูกต้อง จำกัด ข้อความค้นหาที่มีประสิทธิภาพสูงสุดใช้เวลา <0.2 ms เวลาในการวางแผนเป็นมากกว่าเวลาดำเนินการ การทดสอบที่มี 100k หรือมากกว่านั้นจะเป็นการเปิดเผยเพิ่มเติม

ดึงทำไมเพียงชื่อแรกของผู้เขียน? เมื่อคุณรับหลายคอลัมน์แล้วคุณมีสถานการณ์ที่แตกต่างกันเล็กน้อย

ORDER BY titleWHERE title = 'ACADEMY DINOSAUR'ทำให้รู้สึกไม่ในขณะที่การกรองสำหรับชื่อเรื่องเดียวกับ อาจจะORDER BY film_id?

และสำหรับรันไทม์ทั้งหมดแทนที่จะใช้EXPLAIN (ANALYZE, TIMING OFF)เพื่อลดเสียงรบกวน (อาจทำให้เข้าใจผิด) ด้วยค่าใช้จ่ายระยะเวลาย่อย

ตอบ

เป็นการยากที่จะสร้างกฎง่ายๆง่ายๆเนื่องจากประสิทธิภาพโดยรวมขึ้นอยู่กับหลายปัจจัย แนวทางพื้นฐานมาก:

  • การรวมแถวทั้งหมดในตารางย่อยจะมีค่าใช้จ่ายน้อยลง แต่จ่ายเมื่อคุณต้องการแถวทั้งหมดเท่านั้น (หรือส่วนที่มีขนาดใหญ่มาก)

  • สำหรับการเลือกไม่กี่แถว (การทดสอบของคุณ!) เทคนิคการสืบค้นที่แตกต่างกันจะให้ผลลัพธ์ที่ดีกว่า นั่นคือที่LATERALมามันดำเนินการค่าใช้จ่ายเพิ่มเติม แต่อ่านแถวที่ต้องการจากตารางย่อย การชนะครั้งใหญ่หากต้องการเพียงเศษเสี้ยวเล็กน้อย (มาก)

สำหรับกรณีทดสอบเฉพาะของคุณฉันจะทดสอบตัวสร้าง ARRAY ในLATERALแบบสอบถามย่อยด้วย :

SELECT f.film_id, f.title, a.actors
FROM   film
LEFT   JOIN LATERAL (
   SELECT ARRAY (
      SELECT a.first_name
      FROM   film_actor fa
      JOIN   actor a USING (actor_id)
      WHERE  fa.film_id = f.film_id
      ) AS actors
   ) a ON true
WHERE  f.title = 'ACADEMY DINOSAUR';
-- ORDER  BY f.title; -- redundant while we filter for a single title 

ขณะที่มีเพียงการรวมอาร์เรย์เดียวในแบบสอบถามย่อยด้านข้างง่ายดำเนิน ARRAY array_agg()คอนสตรัคดีกว่าฟังก์ชันการรวม ดู:

หรือมีคิวรีย่อยที่มีความสัมพันธ์ต่ำสำหรับกรณีง่าย:

SELECT f.film_id, f.title
     , ARRAY (SELECT a.first_name
              FROM   film_actor fa
              JOIN   actor a USING (actor_id)
              WHERE  fa.film_id = f.film_id) AS actors
FROM   film f
WHERE  f.title = 'ACADEMY DINOSAUR';

หรือโดยทั่วไปแล้วเพียงแค่2x LEFT JOINแล้วรวม :

SELECT f.film_id, f.title, array_agg(a.first_name) AS actors
FROM   film f
LEFT   JOIN film_actor fa USING (film_id)
LEFT   JOIN actor a USING (actor_id)
WHERE  f.title = 'ACADEMY DINOSAUR'
GROUP  BY f.film_id;

ทั้งสามนี้ดูเร็วที่สุดในซอที่ได้รับการอัพเดต (การวางแผน + เวลาดำเนินการ)

ความพยายามครั้งแรกของคุณ (แก้ไขเพียงเล็กน้อยเท่านั้น) โดยทั่วไปแล้วจะเร็วที่สุดในการดึงข้อมูลภาพยนตร์ทั้งหมดหรือส่วนใหญ่แต่ไม่ใช่สำหรับการเลือกขนาดเล็ก:

SELECT f.film_id, f.title, a.actors
FROM   film f
LEFT   JOIN (         
   SELECT fa.film_id, array_agg(first_name) AS actors
   FROM   actor
   JOIN   film_actor fa USING (actor_id)
   GROUP  by fa.film_id
   ) a USING (film_id)
WHERE  f.title = 'ACADEMY DINOSAUR';  -- not good for a single (or few) films!

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.