यह LEFT JOIN LEFT JOIN LATERAL की तुलना में इतना खराब क्यों है?


13

मेरे पास निम्न तालिकाएँ हैं (सकीला डेटाबेस से ली गई हैं):

  • फ़िल्म: film_id पेक है
  • अभिनेता: अभिनेता_निधि है
  • film_actor: film_id और actor_id फिल्म / अभिनेता के लिए fkeys हैं

मैं एक विशेष फिल्म का चयन कर रहा हूं। इस फिल्म के लिए, मैं भी चाहता हूं कि सभी कलाकार उस फिल्म में भाग लें। मेरे पास इसके लिए दो प्रश्न हैं: एक के साथ LEFT JOINऔर एक के साथ LEFT 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-बिट

पर्यावरण: डॉकर docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila:। कृपया ध्यान दें कि डॉकर हब पर छवि पुरानी है, इसलिए मैंने पहले स्थानीय रूप से एक निर्माण किया था: build -t frantiseks/postgres-sakilaगिट रिपॉजिटरी को क्लोन करने के बाद।

तालिका परिभाषाएँ:

फ़िल्म

 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

डेटा: यह सकिला नमूना डेटाबेस से है। यह सवाल वास्तविक जीवन का मामला नहीं है, मैं इस डेटाबेस का उपयोग ज्यादातर लर्निंग सैंपल डेटाबेस के रूप में कर रहा हूं। मुझे कुछ महीने पहले एसक्यूएल में पेश किया गया है और मैं अपने ज्ञान का विस्तार करने की कोशिश कर रहा हूं। इसके निम्न वितरण हैं:

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
एक और बात: सभी महत्वपूर्ण जानकारी प्रश्न में जानी चाहिए (आपकी फिडल लिंक सहित)। कोई भी बाद में सभी टिप्पणियों के माध्यम से नहीं पढ़ना चाहेगा (या वे वैसे भी एक बहुत ही सक्षम मॉडरेटर द्वारा हटा दिए जाते हैं)।
एरविन ब्रान्डेसटेटर

फिडल को सवाल में जोड़ा जाता है!
जेली ऑर्न्स

जवाबों:


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)या पीके बनाएं (film_id, actor_id), जैसे मैं नीचे करता हूं। देख:

    केवल पढ़ने के लिए (या अधिकतर, या आम तौर पर जब VACUUM लेखन गतिविधि के साथ रख सकते हैं) यह (title, film_id)केवल सूचकांक को स्कैन करने की अनुमति देने के लिए एक सूचकांक रखने में मदद करता है। मेरा परीक्षण मामला अब पठन प्रदर्शन के लिए अत्यधिक अनुकूलित है।

  • film.film_id( integer) और film_actor.film_id( smallint) के बीच बेमेल टाइप करें । जबकि यह काम करता है यह प्रश्नों को धीमा बनाता है और विभिन्न जटिलताओं को जन्म दे सकता है। इसके अलावा एफके बाधाओं को और अधिक महंगा बनाता है। ऐसा कभी न करें अगर इससे बचा जा सकता है। यदि आप सुनिश्चित नहीं कर रहे हैं, लेने integerके ऊपर smallint। जबकि प्रति क्षेत्र 2 बाइट्स बचा smallint सकते हैं (अक्सर संरेखण पैडिंग द्वारा खपत) से अधिक जटिलताएं होती हैं integer

  • परीक्षण के प्रदर्शन का अनुकूलन करने के लिए, बहुत सी पंक्तियों को सम्मिलित करने के बाद अनुक्रमित और बाधाएं बनाएं । मौजूदा अनुक्रमितों की तुलना में वर्तमान में सभी पंक्तियों के साथ खरोंच से बनाने के लिए वृद्धावस्था को जोड़ने के लिए यह काफी धीमा है।

इस परीक्षण से संबंधित:

  • बहुत सरल और अधिक विश्वसनीय serial(या IDENTITY) स्तंभों के बजाय मुक्त-खड़े अनुक्रम प्लस स्तंभ चूक । मत करो।

  • timestamp without timestampआम तौर पर एक स्तंभ की तरह अविश्वसनीय है last_updatetimestamptzइसके बजाय उपयोग करें । और ध्यान दें कि स्तंभ चूक करते नहीं "अंतिम अद्यतन" को कवर, सख्ती से बोला।

  • लंबाई संशोधक character varying(255)इंगित करता है कि पोस्टग्रेज के लिए परीक्षण का मामला शुरू करने का इरादा नहीं है क्योंकि विषम लंबाई यहां बहुत व्यर्थ है। (या लेखक क्लूलेस हैं।)

फिडेल में अंकेक्षित परीक्षण मामले पर विचार करें:

db <> यहाँ fiddle - अपने फ़िडेल पर निर्माण, अनुकूलित और अतिरिक्त प्रश्नों के साथ।

सम्बंधित:

1000 फिल्मों और 200 अभिनेताओं के साथ एक परीक्षण सेटअप की वैधता सीमित है। सबसे कुशल प्रश्न <0.2 एमएस। योजना समय निष्पादन समय से अधिक है। 100k या अधिक पंक्तियों के साथ एक परीक्षण अधिक खुलासा होगा।

लेखकों के पहले नामों को ही क्यों पुनः प्राप्त किया जाता है ? एक बार जब आप कई कॉलम पुनः प्राप्त करते हैं, तो आपके पास पहले से ही थोड़ी अलग स्थिति होती है।

ORDER BY titleकिसी भी शीर्षक के लिए फ़िल्टर करते समय कोई मतलब नहीं है WHERE 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!

अधिक बड़ी कार्डिनैलिटी वाले टेस्ट अधिक खुलासा करेंगे। और परिणामों को हल्के ढंग से सामान्य न करें, कुल प्रदर्शन के लिए कई कारक हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.