相关子查询没有预期的行为
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_status
的 t_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_status
table中的status_transition_message_id
是status_transition_message
table的外键,这个字段可以为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
;
我在 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_status
的 t_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_status
table中的status_transition_message_id
是status_transition_message
table的外键,这个字段可以为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
;