web-dev-qa-db-ja.com

条件付きサブクエリ

次のクエリがあります。

SELECT id,
   email,
   first_name as "firstName",
   last_name as "lastName",
   is_active as "isActive",
   password,
   access,
   CASE
     WHEN access < 3 THEN (
       SELECT
         CASE WHEN count(*) = 1 THEN true ELSE false END
       FROM user_rating_entity ure
       WHERE ure.user_id = u.id
         AND ure.rating_entity_id = :re_id
     )
     ELSE true
   END as "isResponsible"
FROM users u
WHERE u.id = :id

access > 3の場合、フィールド「isResponsible」を直接trueに設定し、サブクエリを実行しないでください。 >=および<ににアクセスする両方のケースで説明分析を使用しましたが、同じ出力が得られます。

どうしてこんなことに?

3
Oliver Gibson

ここでクエリプランを読み取るには、3つの重要な部分があります。

  • 走った?もしそうなら、
  • 何回?
  • 相関関係はありましたか?

サンプルデータ

サンプルデータを提供しなかったので、いくつか作成しましょう。

_CREATE TABLE foo AS
SELECT x FROM generate_series(1,100) AS x;
_

そして、実行可能な範囲外で、サブクエリを使用して基本的なクエリを実行してみましょう。

_EXPLAIN ANALYZE
SELECT
  x,
  (CASE WHEN x>200 THEN (SELECT sum(x) FROM foo) END)
FROM foo;
_

計画は、ケースが付随しているが実行されなかったことを示します。

_ Seq Scan on foo  (cost=2.26..4.51 rows=100 width=4) (actual time=0.017..0.047 rows=100 loops=1)
   InitPlan 1 (returns $0)
     ->  Aggregate  (cost=2.25..2.26 rows=1 width=4) (never executed)
           ->  Seq Scan on foo foo_1  (cost=0.00..2.00 rows=100 width=4) (never executed)
 Planning time: 0.101 ms
 Execution time: 0.118 ms
(6 rows)
_

Aggregate行の(never execution)で確認できます。ただし、CASE WHEN x>20 THEN (SELECT sum(x) FROM fooのように設定すると、さらに多くのことがわかります

_ Seq Scan on foo  (cost=2.26..4.51 rows=100 width=4) (actual time=0.020..0.095 rows=100 loops=1)
   InitPlan 1 (returns $0)
     ->  Aggregate  (cost=2.25..2.26 rows=1 width=4) (actual time=0.043..0.043 rows=1 loops=1)
           ->  Seq Scan on foo foo_1  (cost=0.00..2.00 rows=100 width=4) (actual time=0.006..0.019 rows=100 loops=1)
 Planning time: 0.092 ms
 Execution time: 0.158 ms
(6 rows)
_

ここでは、Aggregateが_loops=1_時間ループしていることがわかります。 PostgreSQLは、これが相関サブクエリではなく、単なるリテラルに(本質的に)リテラルに変換されることを認識しています。それが相関していることを確認しましょう。

_EXPLAIN ANALYZE
SELECT
  x,
  (CASE WHEN x>20 THEN (SELECT sum(f2.x)+f1.x FROM foo AS f2) END)
FROM foo AS f1;
_

今あなたはこの計画を見るでしょう

_ Seq Scan on foo f1  (cost=0.00..228.50 rows=100 width=4) (actual time=0.020..3.210 rows=100 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=2.25..2.26 rows=1 width=4) (actual time=0.038..0.038 rows=1 loops=80)
           ->  Seq Scan on foo f2  (cost=0.00..2.00 rows=100 width=4) (actual time=0.005..0.017 rows=100 loops=80)
 Planning time: 0.104 ms
 Execution time: 3.272 ms
_

ここで重要なのは、集計に_loops=80_があり、それ自体に_loops=80_シーケンススキャンが必要であることです。

これはすべて一般的ですが、サンプルデータやクエリプランなしで提供できるのはこれだけです。

5
Evan Carroll

Evanはすでに指摘しています _(never executed)_の出力で_EXPLAIN ANALYZE_を見落とした可能性があります。

最新のPostgresでクエリを記述するためのよりクリーンで用途の広い方法は、LATERALサブクエリを使用することです(必ずしも高速ではありません)。

_SELECT id, email
     , first_name AS "firstName"
     , last_name  AS "lastName"
     , is_active  AS "isActive"
     , password, access
     , COALESCE(ure.resp, true) AS "isResponsible"
FROM  users u
LEFT  JOIN LATERAL (
   SELECT (count(*) = 1) AS resp
   FROM   user_rating_entity
   WHERE  user_id = u.id  -- lateral reference
   AND    rating_entity_id = :re_id
   ) ure ON u.access < 3
WHERE  u.id = :id;
_

コメントしたようにCOALESCE() は、特定の CASE 式のよりエレガントな代替品です。

しかし、あなたはcount(*)は決してnullではないと述べましたか?次に、なぜCOALESCE()

count(*)自体は決してnull(「行なし」の場合は0)ではありませんが、_LEFT JOIN_は、結合条件が満たされない場合でもnullを生成します。そして、それがここでの要点です。Postgresは、結合条件_u.access < 3_が満たされていない外側の行をカウントしません。元のクエリに従って、nullを取得し、trueに折りたたみます。

Evan's answer でわかるように、count(*)は、条件を満たすusersのすべての行に対して_user_rating_entity_の順次スキャンをトリガーします。小さなテーブルや非常に少数のユーザーには問題ありませんが、大きなテーブルには問題があります。

一致するインデックスを使用すると、インデックススキャンを実行できるため、大幅に高速化できます。

_CREATE INDEX foo ON user_rating_entity (user_id, rating_entity_id)
_

また、カウントごとに数行以上ある場合は、より高速なクエリ手法があります。しかし、それがこの質問の範囲を広げています...

関連:


または:

_SELECT ...
     , CASE WHEN ure.ct <> 1 THEN false ELSE true END AS "isResponsible"
    -- COALESCE(NOT ure.ct <> 1, true)  -- equivalent
    -- ure.ct = 1                       -- NOT equivalent, misses NULL case
FROM  users u
LEFT  JOIN LATERAL (
   SELECT count(*) AS ct
   FROM   ...
   ) ure ON u.access < 3
WHERE  u.id = :id;
_

同じ結果。

5