なぜこのLEFT JOINがLEFT JOIN LATERALよりもパフォーマンスがそれほど悪いのですか?


13

次の表があります(Sakilaデータベースから取得)。

  • film:film_idはpkeyです
  • 俳優:actor_idはpkeyです
  • film_actor:film_idとactor_idは、映画/俳優のfkeyです

特定の映画を選択しています。この映画では、すべての俳優がその映画に参加することも望んでいます。これには2つのクエリがあります。1つのクエリLEFT JOINと1 つのクエリです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;

クエリプランを比較すると、最初のクエリのパフォーマンスは2番目のクエリよりもはるかに悪い(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

バージョン:gcc(Alpine 6.4.0)6.4.0、64ビットでコンパイルされたx86_64-pc-linux-musl上のPostgreSQL 10.4

環境:ドッカー: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
もう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)か、PKを作成(film_id, actor_id)します。見る:

    読み取り専用(またはほとんど、または一般的にVACUUMが書き込みアクティビティに対応できる場合)では、インデックスをオンに(title, film_id)してインデックスのみのスキャンを許可することも役立ちます。私のテストケースは、読み取りパフォーマンスに対して高度に最適化されています。

  • 型の不一致との間にfilm.film_idinteger)とfilm_actor.film_idsmallint)。それは一方で働くことは、クエリ遅くなり、様々な合併症を引き起こすことができます。また、FK制約がより高価になります。回避できる場合は、絶対に実行しないでください。よくわからない場合は、を選んintegerでくださいsmallint。ながらsmallint することができ、フィールドごとに2つのバイトを保存するよりも、より複雑である(多くの場合、アライメントパディングによって消費されます)integer

  • テスト自体のパフォーマンスを最適化するには、インデックスと制約を作成します 大量の行を一括挿入した後に。すべての行が存在する状態で最初から作成するよりも、既存のインデックスにタプルを増分的に追加する方が大幅に遅くなります。

このテストとは無関係:

  • はるかに単純で信頼性の高い独立したシーケンスと列のデフォルト serial(またはIDENTITY)列では。しないでください。

  • timestamp without timestampは、通常、のような列に対しては信頼できませんlast_updatetimestamptz代わりに使用してください。そして、列のデフォルトは、厳密に言えば、「最終更新」をカバーない

  • の長さ修飾子 character varying(255)、奇数の長さはここではかなり無意味であるため、テストケースがPostgresで始まることを意図していないことを示します。(または、著者は無知です。)

フィドルの監査済みテストケースを検討します。

db <> fiddle here -最適化されたあなたのフィドル、上、コメントを追加しましたクエリを構築します。

関連:

1000本の映画と200人の俳優によるテスト設定の有効性は限られています。最も効率的なクエリの所要時間は0.2ミリ秒未満です。計画時間は実行時間以上です。10万行以上のテストでは、より明らかになります。

著者の名だけを取得するのなぜですか?複数の列を取得すると、すでに少し状況が異なります。

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

または、非常に基本的に、2倍に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;

これらの3つは、更新されたフィドル(計画+実行時間)で最速のようです。

最初の試み(わずかに変更のみ)は通常、すべてまたはほとんどの映画を取得するのに最も高速ですが、小さな選択ではありません:

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.