Re: BUG #19710: Incorrect DELETE result after LEFT JOIN optimization - Mailing list pgsql-bugs
| From | Ayush Tiwari |
|---|---|
| Subject | Re: BUG #19710: Incorrect DELETE result after LEFT JOIN optimization |
| Date | |
| Msg-id | CAJTYsWWsRMbCPZyLYK7ywjPh3h0THNyj44kGo2F8Mib-OZGeYA@mail.gmail.com Whole thread |
| In response to | BUG #19710: Incorrect DELETE result after LEFT JOIN optimization (PG Bug reporting form <noreply@postgresql.org>) |
| Responses |
Re: BUG #19710: Incorrect DELETE result after LEFT JOIN optimization
|
| List | pgsql-bugs |
Hi,
On Mon, 21 Sept 2026 at 12:05, PG Bug reporting form <noreply@postgresql.org> wrote:
>
> The following bug has been logged on the website:
>
> Bug reference: 19710
> Logged by: cl hl
> Email address: 2320415112@qq.com
> PostgreSQL version: 17.10
> Operating system: Linux LAPTOP-2SQAVLB0 6.6.87.2-microsoft-standard-
> Description:
>
> ## Description
>
> On PostgreSQL 17.10, a `DELETE` statement containing an `EXISTS` subquery
> with two `LEFT JOIN`s deletes a row even though the subquery predicate is
> false.
>
> The test contains one source row whose `c2` value is `sample_b`, while the
> query compares it with the constant `sample_a`. Therefore, the `EXISTS`
> condition should be false. However, PostgreSQL returns and deletes the
> target row.
>
> Tested version:
>
> ```text
> PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) on x86_64-pc-linux-gnu
> ```
>
> ## How to reproduce
>
> Run the following script in a new session. The transaction is rolled back at
> the end, so it does not leave any objects behind.
>
> ```sql
> BEGIN;
>
> CREATE TEMP TABLE t1 (
> c1 text,
> c2 text
> );
>
> CREATE TEMP TABLE t2 (
> c1 text,
> c2 text,
> UNIQUE (c2, c1)
> );
>
> CREATE TEMP TABLE t3 (
> c1 integer PRIMARY KEY,
> c2 integer
> );
>
> INSERT INTO t1 VALUES ('sample_key', 'sample_b');
> INSERT INTO t3 VALUES (1, 0);
>
> SAVEPOINT initial_state;
>
> -- Test query
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 = 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> SELECT * FROM t3;
>
> ROLLBACK TO SAVEPOINT initial_state;
>
> -- Control query: only `=` is changed to `>=`.
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 >= 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> SELECT * FROM t3;
>
> ROLLBACK;
> ```
>
> Both statements run from the same state. Since `t2` is empty, changing `=`
> to
> `>=` cannot change the result of either `LEFT JOIN` for this data.
>
> The issue can also be seen by comparing the execution plans:
>
> ```sql
> EXPLAIN (COSTS OFF)
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 = 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> EXPLAIN (COSTS OFF)
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 >= 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
> ```
>
> Observed plan for the test query (`=`):
>
> ```text
> Delete on t3
> InitPlan 1
> -> Seq Scan on t1
> -> Result
> One-Time Filter: (InitPlan 1).col1
> -> Seq Scan on t3
> ```
>
> Observed plan for the control query (`>=`):
>
> ```text
> Delete on t3
> InitPlan 1
> -> Hash Left Join
> Hash Cond: (t1.c1 = t2.c1)
> Filter: (t1.c2 = 'sample_a'::text)
> -> Seq Scan on t1
> -> Hash
> -> Bitmap Heap Scan on t2
> Recheck Cond: (c2 >= 'sample_a'::text)
> -> Bitmap Index Scan on t2_c2_c1_key
> Index Cond: (c2 >= 'sample_a'::text)
> -> Result
> One-Time Filter: (InitPlan 1).col1
> -> Seq Scan on t3
> ```
>
> ## Expected behavior
>
> The predicate inside the `EXISTS` subquery is logically equivalent to:
>
> ```text
> t1.c2 = 'sample_a'
> ```
>
> The only row in `t1` has `c2 = 'sample_b'`, and `t2` is empty. Therefore,
> the `EXISTS` condition should be false.
>
> Both `DELETE` statements should return no rows:
>
> ```text
> c1
> ----
> (0 rows)
> ```
>
> After each statement, the target row should remain:
>
> ```text
> c1 | c2
> ----+----
> 1 | 0
> (1 row)
> ```
>
> The plan should preserve or derive a restriction equivalent to:
>
> ```text
> Filter: (c2 = 'sample_a'::text)
> ```
>
> ## Actual behavior
>
> The test query using `=` returns and deletes `c1 = 1`:
>
> ```text
> c1
> ----
> 1
> (1 row)
>
> DELETE 1
> ```
>
> The following `SELECT` returns no rows:
>
> ```text
> c1 | c2
> ----+----
> (0 rows)
> ```
>
> After restoring the same initial state, the control query using `>=` returns
> no
> rows and leaves `(1, 0)` in `t3`:
>
> ```text
> c1
> ----
> (0 rows)
>
> DELETE 0
>
> c1 | c2
> ----+----
> 1 | 0
> (1 row)
> ```
>
> The test-query plan scans `t1` without the required `c2 = 'sample_a'`
> filter.
> The control-query plan retains that filter. As a result, only the `=` form
> makes
> the `EXISTS` condition true and causes an incorrect persistent-state change.
Thanks for the report.
AFAICS this has been fixed[1] and will be part of next minor versions.
Regards,
Ayush
[1] https://github.com/postgres/postgres/commit/2ebf25e7d70a8fce31ace78d723fa9271ab8af72
On Mon, 21 Sept 2026 at 12:05, PG Bug reporting form <noreply@postgresql.org> wrote:
>
> The following bug has been logged on the website:
>
> Bug reference: 19710
> Logged by: cl hl
> Email address: 2320415112@qq.com
> PostgreSQL version: 17.10
> Operating system: Linux LAPTOP-2SQAVLB0 6.6.87.2-microsoft-standard-
> Description:
>
> ## Description
>
> On PostgreSQL 17.10, a `DELETE` statement containing an `EXISTS` subquery
> with two `LEFT JOIN`s deletes a row even though the subquery predicate is
> false.
>
> The test contains one source row whose `c2` value is `sample_b`, while the
> query compares it with the constant `sample_a`. Therefore, the `EXISTS`
> condition should be false. However, PostgreSQL returns and deletes the
> target row.
>
> Tested version:
>
> ```text
> PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) on x86_64-pc-linux-gnu
> ```
>
> ## How to reproduce
>
> Run the following script in a new session. The transaction is rolled back at
> the end, so it does not leave any objects behind.
>
> ```sql
> BEGIN;
>
> CREATE TEMP TABLE t1 (
> c1 text,
> c2 text
> );
>
> CREATE TEMP TABLE t2 (
> c1 text,
> c2 text,
> UNIQUE (c2, c1)
> );
>
> CREATE TEMP TABLE t3 (
> c1 integer PRIMARY KEY,
> c2 integer
> );
>
> INSERT INTO t1 VALUES ('sample_key', 'sample_b');
> INSERT INTO t3 VALUES (1, 0);
>
> SAVEPOINT initial_state;
>
> -- Test query
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 = 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> SELECT * FROM t3;
>
> ROLLBACK TO SAVEPOINT initial_state;
>
> -- Control query: only `=` is changed to `>=`.
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 >= 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> SELECT * FROM t3;
>
> ROLLBACK;
> ```
>
> Both statements run from the same state. Since `t2` is empty, changing `=`
> to
> `>=` cannot change the result of either `LEFT JOIN` for this data.
>
> The issue can also be seen by comparing the execution plans:
>
> ```sql
> EXPLAIN (COSTS OFF)
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 = 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
>
> EXPLAIN (COSTS OFF)
> WITH t4 AS (
> SELECT 'sample_a' AS c1
> )
> DELETE FROM t3
> WHERE EXISTS (
> SELECT 1
> FROM t1
> LEFT JOIN t2
> ON t2.c1 = t1.c1
> AND t2.c2 >= 'sample_a'
> LEFT JOIN t4 ON true
> WHERE t1.c2 = t4.c1
> )
> RETURNING c1;
> ```
>
> Observed plan for the test query (`=`):
>
> ```text
> Delete on t3
> InitPlan 1
> -> Seq Scan on t1
> -> Result
> One-Time Filter: (InitPlan 1).col1
> -> Seq Scan on t3
> ```
>
> Observed plan for the control query (`>=`):
>
> ```text
> Delete on t3
> InitPlan 1
> -> Hash Left Join
> Hash Cond: (t1.c1 = t2.c1)
> Filter: (t1.c2 = 'sample_a'::text)
> -> Seq Scan on t1
> -> Hash
> -> Bitmap Heap Scan on t2
> Recheck Cond: (c2 >= 'sample_a'::text)
> -> Bitmap Index Scan on t2_c2_c1_key
> Index Cond: (c2 >= 'sample_a'::text)
> -> Result
> One-Time Filter: (InitPlan 1).col1
> -> Seq Scan on t3
> ```
>
> ## Expected behavior
>
> The predicate inside the `EXISTS` subquery is logically equivalent to:
>
> ```text
> t1.c2 = 'sample_a'
> ```
>
> The only row in `t1` has `c2 = 'sample_b'`, and `t2` is empty. Therefore,
> the `EXISTS` condition should be false.
>
> Both `DELETE` statements should return no rows:
>
> ```text
> c1
> ----
> (0 rows)
> ```
>
> After each statement, the target row should remain:
>
> ```text
> c1 | c2
> ----+----
> 1 | 0
> (1 row)
> ```
>
> The plan should preserve or derive a restriction equivalent to:
>
> ```text
> Filter: (c2 = 'sample_a'::text)
> ```
>
> ## Actual behavior
>
> The test query using `=` returns and deletes `c1 = 1`:
>
> ```text
> c1
> ----
> 1
> (1 row)
>
> DELETE 1
> ```
>
> The following `SELECT` returns no rows:
>
> ```text
> c1 | c2
> ----+----
> (0 rows)
> ```
>
> After restoring the same initial state, the control query using `>=` returns
> no
> rows and leaves `(1, 0)` in `t3`:
>
> ```text
> c1
> ----
> (0 rows)
>
> DELETE 0
>
> c1 | c2
> ----+----
> 1 | 0
> (1 row)
> ```
>
> The test-query plan scans `t1` without the required `c2 = 'sample_a'`
> filter.
> The control-query plan retains that filter. As a result, only the `=` form
> makes
> the `EXISTS` condition true and causes an incorrect persistent-state change.
Thanks for the report.
AFAICS this has been fixed[1] and will be part of next minor versions.
Regards,
Ayush
[1] https://github.com/postgres/postgres/commit/2ebf25e7d70a8fce31ace78d723fa9271ab8af72
pgsql-bugs by date: