0

プロファイル用とプロファイルの雇用状況用の 2 つのテーブルがあります。2 つのテーブルには 1 対 1 の関係があります。1 つのプロファイルに雇用状況がない場合があります。テーブル スキーマは次のとおりです (わかりやすくするために、無関係な列は削除されています)。

create type employment_status as enum ('claimed', 'approved', 'denied');

create table if not exists profiles
(
    id bigserial not null
        constraint profiles_pkey
            primary key
);

create table if not exists employments
(
    id bigserial not null
        constraint employments_pkey
            primary key,
    status employment_status not null,
    profile_id bigint not null
        constraint fk_rails_d95865cd58
            references profiles
                on delete cascade
);

create unique index if not exists index_employments_on_profile_id
    on employments (profile_id);

これらの表を使用して、すべての失業者のプロファイルをリストするように求められました。失業者プロファイルは、雇用記録を持たないプロファイル、または「承認済み」以外のステータスで雇用されているプロファイルとして定義されます。

私の最初の暫定的なクエリは次のクエリでした。

SELECT * FROM "profiles" 
LEFT JOIN employments ON employments.profile_id = profiles.id 
WHERE employments.status != 'approved'

ここでの前提は、すべてのプロファイルがそれぞれの雇用とともにリストされることであり、その後、where 条件でそれらをフィルタリングできます。雇用記録のないプロファイルは、雇用状況がnullであるため、条件によってフィルタリングされます。ただし、このクエリは、雇用されていないプロファイルを返しませんでした。

いくつかの調査の後、この投稿を見つけ、それが機能しない理由を説明し、クエリを変換しました。

SELECT *
FROM profiles
LEFT JOIN employments ON profiles.id = employments.profile_id and employments.status != 'approved';

これは実際に機能しました。しかし、私の ORM はわずかに異なるクエリを生成し、うまくいきませんでした。

SELECT profiles.* FROM "profiles" 
LEFT JOIN employments ON employments.profile_id = profiles.id AND employments.status != 'approved'

唯一の違いはselect句です。このわずかな違いがなぜこのような違いを生むのかを理解しようとしました。3 つのクエリすべてについて説明と分析を実行しました。

EXPLAIN ANALYZE SELECT * FROM "profiles" 
LEFT JOIN employments ON employments.profile_id = profiles.id 
WHERE employments.status != 'approved'

Hash Join  (cost=14.28..37.13 rows=846 width=452) (actual time=0.025..0.027 rows=2 loops=1)
  Hash Cond: (e.profile_id = profiles.id)
  ->  Seq Scan on employments e  (cost=0.00..20.62 rows=846 width=68) (actual time=0.008..0.009 rows=2 loops=1)
        Filter: (status <> ''approved''::employment_status)
        Rows Removed by Filter: 1
  ->  Hash  (cost=11.90..11.90 rows=190 width=384) (actual time=0.007..0.007 rows=8 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 12kB
        ->  Seq Scan on profiles  (cost=0.00..11.90 rows=190 width=384) (actual time=0.003..0.004 rows=8 loops=1)
Planning Time: 0.111 ms
Execution Time: 0.053 ms
EXPLAIN ANALYZE SELECT *
FROM profiles
LEFT JOIN employments ON profiles.id = employments.profile_id and employments.status != 'approved';

Hash Right Join  (cost=14.28..37.13 rows=846 width=452) (actual time=0.036..0.042 rows=8 loops=1)
  Hash Cond: (employments.profile_id = profiles.id)
  ->  Seq Scan on employments  (cost=0.00..20.62 rows=846 width=68) (actual time=0.005..0.005 rows=2 loops=1)
        Filter: (status <> ''approved''::employment_status)
        Rows Removed by Filter: 1
  ->  Hash  (cost=11.90..11.90 rows=190 width=384) (actual time=0.015..0.015 rows=8 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 12kB
        ->  Seq Scan on profiles  (cost=0.00..11.90 rows=190 width=384) (actual time=0.010..0.011 rows=8 loops=1)
Planning Time: 0.106 ms
Execution Time: 0.108 ms
EXPLAIN ANALYZE SELECT profiles.* FROM "profiles" 
LEFT JOIN employments ON employments.profile_id = profiles.id AND employments.status != 'approved'

Seq Scan on profiles  (cost=0.00..11.90 rows=190 width=384) (actual time=0.006..0.007 rows=8 loops=1)
Planning Time: 0.063 ms
Execution Time: 0.016 ms

最初と 2 番目のクエリ プランは、一方のハッシュ結合と他方の右ハッシュ結合を除いてほとんど同じですが、最後のクエリは結合または where 条件さえ実行しません。

私はうまくいった4番目のクエリを思いつきました:

EXPLAIN ANALYZE SELECT profiles.* FROM profiles 
LEFT JOIN employments ON employments.profile_id = profiles.id 
WHERE (employments.id IS NULL OR employments.status != 'approved')

Hash Right Join  (cost=14.28..35.02 rows=846 width=384) (actual time=0.021..0.026 rows=7 loops=1)
  Hash Cond: (employments.profile_id = profiles.id)
  Filter: ((employments.id IS NULL) OR (employments.status <> ''approved''::employment_status))
  Rows Removed by Filter: 1
  ->  Seq Scan on employments  (cost=0.00..18.50 rows=850 width=20) (actual time=0.002..0.003 rows=3 loops=1)
  ->  Hash  (cost=11.90..11.90 rows=190 width=384) (actual time=0.011..0.011 rows=8 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 12kB
        ->  Seq Scan on profiles  (cost=0.00..11.90 rows=190 width=384) (actual time=0.007..0.008 rows=8 loops=1)
Planning Time: 0.104 ms
Execution Time: 0.049 ms

この件に関する私の質問は次のとおりです。

  1. 構造は同じなのに、2 番目と 3 番目のクエリのクエリ プランが異なるのはなぜですか?
  2. 同じ構造なのに、最初と 4 番目のクエリのクエリ プランが異なるのはなぜですか?
  3. Postgres が 3 番目のクエリの join と where 条件を完全に無視するのはなぜですか?

編集:

次のサンプル データでは、予想されるクエリは 2 と 3 を返すはずです。

insert into profiles values (1);
insert into profiles values (2);
insert into profiles values (3);

insert into employments (profile_id, status) values (1, 'approved');
insert into employments (profile_id, status) values (2, 'denied');
4

1 に答える 1

0

の特定の行に関連する の行が最大で 1 つ存在できることをオプティマイザが認識できるように、employments.profile_id(または適切な句を含むビューである)に一意または主キーの制約が存在する必要があります。DISTINCTemploymentsprofiles

employmentsその場合、リストで の列を使用しない場合SELECT、オプティマイザは、結合が冗長であり、計算する必要がないと判断し、より単純で高速な実行計画を作成します。

のコメントを参照してjoin_is_removableくださいsrc/backend/optimizer/plan/analyzejoins.c

/*
 * join_is_removable
 *    Check whether we need not perform this special join at all, because
 *    it will just duplicate its left input.
 *
 * This is true for a left join for which the join condition cannot match
 * more than one inner-side row.  (There are other possibly interesting
 * cases, but we don't have the infrastructure to prove them.)  We also
 * have to check that the inner side doesn't generate any variables needed
 * above the join.
 */
于 2020-09-30T06:43:16.833 に答える