| From: | 陈列行 <2320415112(at)qq(dot)com> |
|---|---|
| To: | pgsql-hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Subject: | Duplicating an equivalent ANY predicate changes a semijoin into a slower per-row SubPlan |
| Date: | 2026-08-17 08:00:46 |
| Message-ID: | tencent_C6C91A262C91D2D5111B48762270194A8A06@qq.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
## Description
This issue concerns an idempotent Boolean rewrite such as `P OR P`, which is
logically equivalent to `P`. PostgreSQL simplifies the displayed filter to a
single predicate, but the redundant SQL form can still receive a different
and slower execution strategy. Three independently generated cases exhibited
the same order-stable slowdown.
### Expected Behaviour
PostgreSQL should canonicalize an idempotent Boolean expression before
selecting the access and join strategy. `P` and `P OR P` should therefore
produce equivalent plans and comparable execution times, particularly when
`P` contains an uncorrelated `ANY` subquery.
### Actual Behaviour
In the standalone case below, the original predicate uses a `Nested Loop
Semi Join`. After the same predicate is duplicated with `OR`, PostgreSQL
uses a sequential scan with a per-row materialized `SubPlan`. Although the
final plan displays only one copy of the predicate, execution time increases
from 3,577.467 ms to 4,157.191 ms, approximately 16.2%.
All three remained slower in both execution orders (`order_stable: True`).
## How to repeat
Run the following complete SQL in a new PostgreSQL session:
```sql
DROP TABLE IF EXISTS duplicate_operand_outer;
DROP TABLE IF EXISTS duplicate_operand_inner;
CREATE TABLE duplicate_operand_outer (
v INTEGER NOT NULL
);
CREATE TABLE duplicate_operand_inner (
v INTEGER NOT NULL
);
INSERT INTO duplicate_operand_outer (v)
SELECT 1
FROM generate_series(1, 1000);
INSERT INTO duplicate_operand_inner (v)
SELECT 1
FROM generate_series(1, 100000);
ANALYZE duplicate_operand_outer;
ANALYZE duplicate_operand_inner;
-- Original predicate P. The result is 0.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF)
SELECT COUNT(*)
FROM duplicate_operand_outer AS o
WHERE o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i## Description
This issue concerns an idempotent Boolean rewrite such as `P OR P`, which is
logically equivalent to `P`. PostgreSQL simplifies the displayed filter to a
single predicate, but the redundant SQL form can still receive a different
and slower execution strategy. Three independently generated cases exhibited
the same order-stable slowdown.
### Expected Behaviour
PostgreSQL should canonicalize an idempotent Boolean expression before
selecting the access and join strategy. `P` and `P OR P` should therefore
produce equivalent plans and comparable execution times, particularly when
`P` contains an uncorrelated `ANY` subquery.
### Actual Behaviour
In the standalone case below, the original predicate uses a `Nested Loop
Semi Join`. After the same predicate is duplicated with `OR`, PostgreSQL
uses a sequential scan with a per-row materialized `SubPlan`. Although the
final plan displays only one copy of the predicate, execution time increases
from 3,577.467 ms to 4,157.191 ms, approximately 16.2%.
All three remained slower in both execution orders (`order_stable: True`).
## How to repeat
Run the following complete SQL in a new PostgreSQL session:
```sql
DROP TABLE IF EXISTS duplicate_operand_outer;
DROP TABLE IF EXISTS duplicate_operand_inner;
CREATE TABLE duplicate_operand_outer (
v INTEGER NOT NULL
);
CREATE TABLE duplicate_operand_inner (
v INTEGER NOT NULL
);
INSERT INTO duplicate_operand_outer (v)
SELECT 1
FROM generate_series(1, 1000);
INSERT INTO duplicate_operand_inner (v)
SELECT 1
FROM generate_series(1, 100000);
ANALYZE duplicate_operand_outer;
ANALYZE duplicate_operand_inner;
-- Original predicate P. The result is 0.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF)
SELECT COUNT(*)
FROM duplicate_operand_outer AS o
WHERE o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i
);
-- Idempotent rewrite P OR P. The result is also 0.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF)
SELECT COUNT(*)
FROM duplicate_operand_outer AS o
WHERE o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i
)
OR o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i
);
```
On the tested version, the characteristic plans and timings are:
```text
P:
Nested Loop Semi Join
Execution Time: 3577.467 ms
P OR P:
Seq Scan on duplicate_operand_outer
Filter: ANY (... SubPlan 1 ...)
Execution Time: 4157.191 ms
```
);
-- Idempotent rewrite P OR P. The result is also 0.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF)
SELECT COUNT(*)
FROM duplicate_operand_outer AS o
WHERE o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i
)
OR o.v <> ANY (
SELECT i.v
FROM duplicate_operand_inner AS i
);
```
On the tested version, the characteristic plans and timings are:
```text
P:
Nested Loop Semi Join
Execution Time: 3577.467 ms
P OR P:
Seq Scan on duplicate_operand_outer
Filter: ANY (... SubPlan 1 ...)
Execution Time: 4157.191 ms
```
| From | Date | Subject | |
|---|---|---|---|
| Next Message | 陈列行 | 2026-08-17 08:03:56 | Redundant outer DISTINCT adds Sort and Unique above INTERSECT |
| Previous Message | 陈列行 | 2026-08-17 07:49:53 | Semantically redundant DISTINCT in an IN subquery changes the join strategy and improves execution |