条件子查询

Oli*_*son 3 postgresql optimization execution-plan subquery explain

我有以下查询:

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
Run Code Online (Sandbox Code Playgroud)

如果access > 3,字段“isResponsible”应直接设置为true,并且不应执行子查询。我在这两种情况下都使用了解释分析,其中 access>=<to 3但我得到了相同的输出。

为什么呢?

Eva*_*oll 5

这里有阅读查询计划的三个重要部分,

  • 跑了吗。如果是这样的话,
  • 多少次?
  • 是相关的吗?

样本数据

您没有提供任何示例数据,所以让我们创建一些。

CREATE TABLE foo AS
SELECT x FROM generate_series(1,100) AS x;
Run Code Online (Sandbox Code Playgroud)

而且,现在让我们在可能的执行范围之外运行带有子查询的基本查询。

EXPLAIN ANALYZE
SELECT
  x,
  (CASE WHEN x>200 THEN (SELECT sum(x) FROM foo) END)
FROM foo;
Run Code Online (Sandbox Code Playgroud)

该计划将表明该案件已被执行,但从未被执行。

 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)
Run Code Online (Sandbox Code Playgroud)

你可以看到 with (never execution)Aggregate行了。但是,如果我们将其设置为类似的内容,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)
Run Code Online (Sandbox Code Playgroud)

在这里我们可以看到 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;
Run Code Online (Sandbox Code Playgroud)

现在你会看到这个计划

 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
Run Code Online (Sandbox Code Playgroud)

这里的关键是聚合loops=80本身需要loops=80seq 扫描。

这都是一般性的,但如果没有您的示例数据或查询计划,我只能给出这些。


Erw*_*ter 5

埃文已经指出,你可能忽略了(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;
Run Code Online (Sandbox Code Playgroud)

就像我评论的那样COALESCE()是您特定CASE表达的更优雅的替代品。

但你说count(*)是从来没有null?那为什么COALESCE()

即使它count(*)本身从不null(0 表示“无行”),它LEFT JOIN仍然会null在不满足连接条件的情况下产生。这就是这里的重点:Postgres 不计算u.access < 3不满足连接条件的外部行。我们根据您的原始查询将其null折叠为true

正如我们在看到埃文的回答count(*)触发顺序扫描的user_rating_entity在每一行users有资格。对于小表或很少的用户来说还可以,但对于大表来说是有问题的。

使用匹配索引以允许索引扫描可以显着更快:

CREATE INDEX foo ON user_rating_entity (user_id, rating_entity_id)
Run Code Online (Sandbox Code Playgroud)

如果每个计数的行数超过几行,则有更快的查询技术。但这扩大了这个问题的范围......

有关的:


或者:

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;
Run Code Online (Sandbox Code Playgroud)

结果一样。