web-dev-qa-db-ja.com

このLEFT JOINのパフォーマンスがLEFT JOIN LATERALよりもはるかに悪いのはなぜですか?

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

  • film:film_idはpkeyです
  • actor:actor_idはpkeyです
  • film_actor:film_idとactor_idは、film/actorの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番目のクエリよりもパフォーマンスがかなり低下します(20倍)。

_ 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_クエリでは、データベース内のすべての映画に対してサブクエリが実行されているように見えます。1つの特定の映画のみに関心がある外部クエリでのフィルタリングは考慮されていません。なぜプランナはサブクエリでその知識を持つことができないのですか?

_LEFT JOIN LATERAL_クエリでは、フィルターを下方に「プッシュ」しています。したがって、最初のクエリで発生した問題はここには存在しないため、パフォーマンスが向上します。

私は主に経験則、一般的な知恵を探していると思います...したがって、このプランナーマジックは、第2の性質になります。

更新(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

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

環境:Docker:_docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila_。 Dockerハブのイメージは古くなっているので、gitリポジトリのクローンを作成した後、_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
_

データ:これは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
_
13
Jelly Orns

テスト設定

元の設定 フィドル内 には改善の余地があります。私は理由のためにあなたのセットアップを求め続けました。

  • _film_actor_に次のインデックスがあります:

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

    これはすでにかなり役に立ちます。ただし、特定のクエリを最も効果的にサポートするには、_(film_id, actor_id)_multicolumn indexの列をこの順序で配置します。実用的な解決策:_idx_fk_film_id_を_(film_id, actor_id)_のインデックスに置き換える-または、以下のように、このテストの目的で_(film_id, actor_id)_にPKを作成します。見る:

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

  • タイプの不一致 _film.film_id_(integer)と_film_actor.film_id_(smallint)の間。worksの場合、クエリが遅くなり、さまざまな問題が発生する可能性があります。また、FK制約のコストが高くなります。回避できる場合は、これを行わないでください。よくわからない場合は、integerよりもsmallintを選択してください。 smallintcanはフィールドごとに2バイトを節約しますが(多くの場合、アライメントパディングによって消費されます)、integerを使用する場合よりも複雑になります。

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

このテストとは無関係:

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

  • _timestamp without timestamp_は通常、_last_update_のような列では信頼できません。代わりにtimestamptzを使用してください。また、厳密に言えば、列のデフォルトはnotが「最終更新」をカバーすることに注意してください。

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

フィドルの監査済みテストケースを考えてみましょう。

db <> fiddle here-フィドルを基に構築し、最適化し、クエリを追加します。

関連:

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

なぜonly著者の名を取得するのですか?複数の列を取得すると、わずかに異なる状況になります。

_ORDER BY title_を使用して単一のタイトルをフィルタリングする場合、_WHERE title = 'ACADEMY DINOSAUR'_は意味がありません。多分_ORDER BY film_id_?

total runtimeの場合は、サブタイミングのオーバーヘッドを伴う(潜在的に誤解を招く可能性のある)ノイズを減らすために、むしろ EXPLAIN (ANALYZE, TIMING OFF) を使用します。

回答

全体のパフォーマンスは多くの要因に依存するため、単純な経験則を形成することは困難です。非常に基本的なガイドライン:

  • サブテーブルでall行を集計するとオーバーヘッドが少なくなりますが、実際にすべての行(または非常に大きな部分)が必要な場合にのみ効果があります。

  • few行を選択する場合(テスト!)、さまざまなクエリ手法を使用すると、より良い結果が得られます。そこがLATERALの出番です。オーバーヘッドが増えますが、必要な行はサブテーブルからのみ読み取られます。 (非常に)小さな部分のみが必要な場合の大きな勝利。

あなたの特定のテストケースでは、LATERALサブクエリのARRAYコンストラクタもテストします。

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

これらの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!
_

カーディナリティがはるかに大きいテストは、より明らかになります。結果を軽く一般化しないでください。全体的なパフォーマンスには多くの要素があります。

7