Bu LEFT JOIN neden LEFT JOIN LATERAL'dan daha kötü performans gösteriyor?


13

(Sakila veritabanından alınan) aşağıdaki tabloları var:

  • film: film_id pkey
  • aktör: actor_id pkey
  • film_actor: film_id ve actor_id, film / aktörün anahtarlarıdır

Belirli bir film seçiyorum. Bu film için, o filme tüm aktörlerin de katılmasını istiyorum. Bunun için iki sorgu var: bir ile bir LEFT JOINve bir ile bir 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;

Sorgu planını karşılaştırırken, ilk sorgu ikinciden çok daha kötü performans gösterir (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

Bu neden? Bu konuda mantık öğrenmek istiyorum, bu yüzden neler olup bittiğini anlayabiliyorum ve veri boyutu arttığında sorgunun nasıl davranacağını ve planlayıcının belirli koşullar altında hangi kararları vereceğini tahmin edebiliyorum.

Düşüncelerim: ilk LEFT JOINsorguda, alt sorgu sadece belirli bir filmle ilgilendiğimiz dış sorgudaki filtreleme dikkate alınmadan veritabanındaki tüm filmler için yürütülüyor gibi görünüyor. Planlayıcı neden alt bilgi sorgularında bu bilgiye sahip olamıyor?

In LEFT JOIN LATERALsorguda, daha az ya da 'itme' filtreleme aşağıya yönünde. İlk sorguda yaşadığımız sorun burada mevcut değil, dolayısıyla daha iyi performans.

Sanırım esas olarak başparmak kurallarını, genel bilgelikleri arıyorum ... bu planlayıcı sihir ikinci doğaya dönüşüyor - eğer mantıklıysa.

güncelleme (1)

LEFT JOINAşağıdaki gibi yeniden yazmak da daha iyi performans sağlar (biraz daha iyi 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

Bu konuda nasıl mantık yapabiliriz?

güncelleme (2)

Bazı deneylerle devam ettim ve ilginç bir kural olduğunu düşünüyorum: toplama işlevini olabildiğince yüksek / geç uygulayın . Güncelleme (1) 'deki sorgu muhtemelen daha iyi performans gösterir çünkü dış sorguda artık iç sorguda toplanmıyoruz.

LEFT JOIN LATERALYukarıdakileri aşağıdaki gibi yeniden yazarsak da aynı şey geçerlidir :

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

Burada array_agg()yukarı doğru hareket ettik . Gördüğünüz gibi, bu plan orijinalinden daha iyidir LEFT JOIN LATERAL.

Bununla birlikte, kendi kendine icat edilen bu başparmak kuralının ( toplama işlevini olabildiğince yüksek / geç uygulayın ) diğer durumlarda doğru olup olmadığından emin değilim .

ek bilgi

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

Sürüm: xgre_QL-pc-linux-musl üzerinde PostgreSQL 10.4, gcc tarafından derlenmiştir (Alpine 6.4.0) 6.4.0, 64-bit

Çevre: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila. Docker hub'ındaki görüntünün eski olduğunu lütfen unutmayın, bu yüzden önce yerel olarak bir yapı yaptım: build -t frantiseks/postgres-sakilagit deposunu klonladıktan sonra.

Tablo tanımları:

film

 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

aktör

 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 oyuncusu

 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

Veri: Bu Sakila örnek veritabanından. Bu soru gerçek hayattaki bir durum değil, bu veritabanını çoğunlukla öğrenme örneği veritabanı olarak kullanıyorum. Birkaç ay önce SQL ile tanıştım ve bilgimi genişletmeye çalışıyorum. Aşağıdaki dağıtımlara sahiptir:

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
Bir şey daha: tüm önemli bilgiler soruya girmelidir (keman bağlantınız dahil). Kimse daha sonra tüm yorumları okumak istemeyecektir (ya da zaten belirli bir yetenekli moderatör tarafından silinir).
Erwin Brandstetter

Keman soruya eklendi!
Jelly Orns

Yanıtlar:


7

Test kurulumu

Orijinal kurulum keman içinde iyileştirilmesi için yapraklar odası. Bir sebepten dolayı kurulumunu istemeye devam ettim.

  • Şu dizinlere sahipsiniz film_actor:

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

    Bu zaten oldukça faydalı. Ancak sorgunuzu en iyi şekilde desteklemek için , bu sırayla sütunlar üzerinde çok sütunlu bir dizininiz olur(film_id, actor_id) . Pratik bir çözüm: idx_fk_film_idbir indeks ile değiştirin (film_id, actor_id)- veya (film_id, actor_id)aşağıda yaptığım gibi bu test için PK'yi oluşturun . Görmek:

    Salt okunur durumda (veya çoğunlukla veya genellikle VACUUM yazma etkinliğine ayak uydurabildiğinde), (title, film_id)yalnızca dizin taramalarına izin vermek için bir dizin oluşturulmasına yardımcı olur . Test durumum şimdi okuma performansı için yüksek oranda optimize edildi.

  • film.film_id( integer) Ve film_actor.film_id( smallint) arasında uyumsuzluk yazın . O iken çalışır onu sorgular yavaşlatır ve çeşitli komplikasyonlara yol açabilir. Ayrıca FK kısıtlamalarını daha pahalı hale getirir. Önlenebilirse bunu asla yapmayın. Emin değilseniz, almak integerüzerine smallint. İken smallint edebilir alan başına 2 bayt kaydetmek ile daha komplikasyon vardır (genellikle hizalama dolgu tarafından tüketilen) integer.

  • Testin performansını en iyi duruma getirmek için, çok sayıda satır toplu olarak ekledikten sonra dizinler ve kısıtlamalar oluşturun . Mevcut dizinlere kademeli olarak tuples eklemek, mevcut tüm satırlarla sıfırdan oluşturmaktan çok daha yavaştır.

Bu testle ilgisi yok:

  • Daha basit ve daha güvenilir serial(veya IDENTITY) sütunlar yerine bağımsız sıralar ve sütun varsayılanları . Yapma.

  • timestamp without timestampgibi bir sütun için genellikle güvenilir değildir last_update. timestamptzBunun yerine kullanın . Ve sütun varsayılan do not o değil , "son güncelleştirme" kapak, tam olarak değil.

  • Uzunluk değiştirici character varying(255), test durumunun Postgres ile başlaması için tasarlanmadığını gösterir çünkü tek uzunluk burada oldukça anlamsızdır. (Ya da yazar clueless.)

Kemandaki denetlenmiş test durumunu düşünün:

db <> burada keman - keman üzerinde inşa, optimize edilmiş ve eklenen sorgular ile.

İlişkili:

1000 film ve 200 oyuncu ile yapılan bir test kurulumunun geçerliliği sınırlıdır. En etkili sorgular <0,2 msn sürer. Planlama süresi yürütme süresinden daha fazladır. 100k veya daha fazla satır içeren bir test daha açıklayıcı olacaktır.

Neden sadece yazarların ilk isimlerini alalım? Birden çok sütun aldığınızda, zaten biraz farklı bir durumunuz olur.

ORDER BY titleile tek bir başlık için filtreleme yaparken hiçbir anlam ifade etmiyor WHERE title = 'ACADEMY DINOSAUR'. Belki ORDER BY film_id?

Toplam çalışma zamanı için EXPLAIN (ANALYZE, TIMING OFF), alt zamanlama yükü ile gürültüyü azaltmak (potansiyel olarak yanıltıcı) için kullanın .

Cevap

Basit bir başparmak kuralı oluşturmak zordur, çünkü toplam performans birçok faktöre bağlıdır. Çok temel kurallar:

  • Alt tablolardaki tüm satırları toplamak daha az yük taşır, ancak yalnızca tüm satırlara (veya çok büyük bir kısma) gerçekten ihtiyacınız olduğunda ödeme yapar.

  • Seçilmesi için kaç (testinizi!) Satırları, farklı sorgu teknikleri daha iyi sonuçlar verir. İşte devreye LATERALgiriyor. Daha fazla yük taşıyor, ancak sadece alt tablolardan gerekli satırları okuyor. Sadece (çok) küçük bir kesire ihtiyaç duyulursa büyük bir kazanç.

Özel test durumunuz için, alt sorguda bir ARRAY yapıcısınıLATERAL da test ederim :

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 

Yanal alt sorguda yalnızca tek bir diziyi toplarken, basit bir ARRAY yapıcısı toplama işlevinden daha iyi performans gösterir array_agg(). Görmek:

Veya basit vaka için düşük korelasyonlu bir alt sorgu ile:

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

Ya da, çok temelde, sadece 2x LEFT JOINve daha sonra :

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;

Bu üçü güncellenmiş kemanımda en hızlı görünüyor (planlama + yürütme süresi).

İlk denemeniz (sadece biraz değiştirilmiş) genellikle filmlerin tümünü veya çoğunu almak için en hızlıdır , ancak küçük bir seçim için değil:

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!

Çok daha büyük kardinaliteleri olan testler daha açıklayıcı olacaktır. Sonuçları hafifçe genellemeyin, toplam performans için birçok faktör vardır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.