相关子查询没有预期的行为

Correlated Subquery not having expected behavior

我在 Postgres 中有这个 SQL 查询,我有一个意外的行为:

SELECT
  DISTINCT s.id,
  (
    SELECT string_agg(CAST(t_code AS TEXT), ',') 
    FROM (
      SELECT DISTINCT ps.status
      FROM products_status ps
      WHERE
        ps.status_transition_message_id IS NOT NULL AND 
        ps.enabled IS FALSE AND
        ps.status_transition_message_id = stm_rejected.id
    ) AS t_code
  ) AS t_code
FROM (
  SELECT
    *,
     row_number() OVER (PARTITION BY stm.shipment_id ORDER BY date) AS rn 
  FROM status_transition_message stm
  WHERE
    stm.final_status = 6 OR
    stm.final_status = 7
) AS stm_rejected
JOIN shipment s ON s.id = stm_rejected.shipment_id
WHERE
  stm_rejected.rn = 1 AND
  stm_rejected.date BETWEEN :startDate AND :endDate;

status_transition_message table 表示给定货件的 X 和 Y 之间的状态转换。通过此查询,我将查看所有货件,并获取在给定开始日期和结束日期之间第一次状态转换为 6 或 7(它们可以多次经历相同转换)的货件。对于符合此条件的发货,我正在为该状态转换中指定的 products_statust_code 字段执行 SELECT

此查询的问题在于,在 t_code 子查询中,即使 ps.status_transition_message_id IS NOT NULL 的计算结果为 false,Postgres 仍在计算 ps.status_transition_message_id = stm_rejected.id,即使它出现在 AND 布尔值之后操作员。我知道这是因为当我从查询中删除 ps.status_transition_message_id = stm_rejected.id 部分时,它执行得更快。而且我也 100% 确定我正在测试的数据库中的每个 products_status 都将 status_transition_message_id 设为 NULL。

编辑:status_transition_message 行如下所示:

[id] [date]         [initial_status]  [final_status]    [shipment_id]

434  3/20/13 14:18   0                4                  943

products_status 行:

[id] [status]  [status_transition_message_id]  [enabled]    [shipment_id]

211  5         434                             true          943

products_statustable中的status_transition_message_idstatus_transition_messagetable的外键,这个字段可以为null,这就是为什么在子查询的 WHERE 语句的第一个子句中,我正在检查它是否为 NULL(因此不必在不需要的情况下针对 stm_rejected 行进行测试)

我不知道我的问题是否清楚,但查询确实 return 了预期的结果。问题在于,当第一个子句的计算结果为 false 时,它​​会不必要地计算 AND 子句,这会损害查询的性能。

您的直觉是正确的:布尔表达式不是从左到右求值的。 postgres 文档中的 expression evaluation rules (4.2.14) 状态:

The order of evaluation of subexpressions is not defined. In particular, the inputs of an operator or function are not necessarily evaluated left-to-right or in any other fixed order.

要强制计算顺序,您可以使用通用的 table 表达式 (CTE),这也会使您的查询更具可读性。它告诉优化器不要重写表达式,而是具体化结果,有点像临时 table。如果没有 运行 并解释对实际数据的查询是否会导致性能提高或降低,则很难判断。我会同时尝试子查询和 CTE。

但是,在您的特定情况下,可能不需要相关的子查询。我已经重写了查询以使用内部联接,这应该可以更有效地处理该逻辑。我在这里也使用了 CTE,但出于不同的目的,因为我猜测到状态 6 和 7 的转换只是所有转换的一小部分,因此性能可能会受益于早期减少行数。

我在这里也遇到了麻烦,更改了逻辑以用显式分组替换 distinct。

with

  stm_rejected as (
    select
      id,
      "date" as transition_date,
      row_number() over (partition by shipment_id order by "date") as transition_rank
    from
      status_transition_message
    where
      final_status in (6, 7)
  )

select
  shipment.id as shipment_id,
  string_agg(products_status.t_code, ',') as t_codes
from
  shipment
  inner join stm_rejected
    on shipment.id = stm_rejected.shipment_id
  inner join products_status
    on stm_rejected.id = products_status.status_transition_message_id
where
  stm_rejected.transition_rank = 1
  and stm_rejected.transition_date between :startDate and :endDate
  and products_status.enabled = false
group by
  shipment.id
;