On 17/04/2026 10:56, Andrei Lepikhov wrote:
> The best-known problematic code example causing this issue is
> apply_scanjoin_target_to_paths(), and the current_rel/final_rel game from
> commit
> 0927d2f46dd. Quickly fixing it, I see some more combinations have emerged:
On closer inspection, it looks like all the detected cases come from the same
issue in create_ordered_paths. The ordered_rel has the same path in its pathlist
as the input_rel. Sometimes, this path is removed and freed from ordered_rel,
which leads to a dangling pointer in the child RelOptInfo.
I've attached a patch that shows how to fix the issue. Some regression tests
change because of a hidden rule where a projection and its subpath have
different target lists. Right now, the patch always enforces a projection, even
if the target lists are the same. This is still open for discussion on whether
there's a better way to handle it.
--
regards, Andrei Lepikhov,
pgEdge
From 3bbde842ad2da44acd47170b3e9949f621102d50 Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <[email protected]>
Date: Mon, 20 Apr 2026 17:25:27 +0200
Subject: [PATCH v0] Do not put one path into different pathlists
---
src/backend/optimizer/plan/planner.c | 34 ++++++++++++++-----
src/test/regress/expected/limit.out | 6 ++--
.../regress/expected/select_distinct_on.out | 26 +++++++-------
src/test/regress/expected/select_parallel.out | 32 ++++++++---------
src/test/regress/expected/tsrf.out | 8 ++---
5 files changed, 60 insertions(+), 46 deletions(-)
diff --git a/src/backend/optimizer/plan/planner.c
b/src/backend/optimizer/plan/planner.c
index 56bb1d798e3..cd3250c9672 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -5462,7 +5462,20 @@ create_ordered_paths(PlannerInfo *root,
input_path->pathkeys, &presorted_keys);
if (is_sorted)
- sorted_path = input_path;
+ {
+ /*
+ * The input_path is already sorted; we would like to
reuse it as
+ * the ordered rel's path. But we must not share the
pointer with
+ * input_rel->pathlist. Wrap it in a fresh
ProjectionPath.
+ */
+ Path *wrap_target = input_path;
+
+ if (IsA(wrap_target, ProjectionPath))
+ wrap_target = ((ProjectionPath *)
wrap_target)->subpath;
+
+ sorted_path = (Path *) create_projection_path(root,
ordered_rel,
+
wrap_target, target);
+ }
else
{
/*
@@ -5494,15 +5507,18 @@ create_ordered_paths(PlannerInfo *root,
root->sort_pathkeys,
presorted_keys,
limit_tuples);
- }
- /*
- * If the pathtarget of the result path has different
expressions from
- * the target to be applied, a projection step is needed.
- */
- if (!equal(sorted_path->pathtarget->exprs, target->exprs))
- sorted_path = apply_projection_to_path(root,
ordered_rel,
-
sorted_path, target);
+ /*
+ * If the pathtarget of the result path has different
expressions
+ * from the target to be applied, a projection step is
needed.
+ * When is_sorted is true the wrap above already
carries the
+ * ordered rel's target, so this only applies to the
sorted
+ * branch.
+ */
+ if (!equal(sorted_path->pathtarget->exprs,
target->exprs))
+ sorted_path = apply_projection_to_path(root,
ordered_rel,
+
sorted_path, target);
+ }
add_path(ordered_rel, sorted_path);
}
diff --git a/src/test/regress/expected/limit.out
b/src/test/regress/expected/limit.out
index e3bcc680653..c12b2498f65 100644
--- a/src/test/regress/expected/limit.out
+++ b/src/test/regress/expected/limit.out
@@ -439,14 +439,14 @@ select currval('testseq');
explain (verbose, costs off)
select unique1, unique2, generate_series(1,10)
from tenk1 order by unique2 limit 7;
- QUERY
PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------
Limit
Output: unique1, unique2, (generate_series(1, 10))
-> ProjectSet
Output: unique1, unique2, generate_series(1, 10)
-> Index Scan using tenk1_unique2 on public.tenk1
- Output: unique1, unique2, two, four, ten, twenty, hundred,
thousand, twothousand, fivethous, tenthous, odd, even, stringu1, stringu2,
string4
+ Output: unique1, unique2
(6 rows)
select unique1, unique2, generate_series(1,10)
diff --git a/src/test/regress/expected/select_distinct_on.out
b/src/test/regress/expected/select_distinct_on.out
index 75b1e7d300f..4ae09c8b181 100644
--- a/src/test/regress/expected/select_distinct_on.out
+++ b/src/test/regress/expected/select_distinct_on.out
@@ -81,12 +81,13 @@ select distinct on (1) floor(random()) as r, f1 from
int4_tbl order by 1,2;
EXPLAIN (COSTS OFF)
SELECT DISTINCT ON (four) four,two
FROM tenk1 WHERE four = 0 ORDER BY 1;
- QUERY PLAN
-----------------------------
- Limit
- -> Seq Scan on tenk1
- Filter: (four = 0)
-(3 rows)
+ QUERY PLAN
+----------------------------------
+ Result
+ -> Limit
+ -> Seq Scan on tenk1
+ Filter: (four = 0)
+(4 rows)
-- and check the result of the above query is correct
SELECT DISTINCT ON (four) four,two
@@ -114,12 +115,13 @@ SELECT DISTINCT ON (four) four,two
EXPLAIN (COSTS OFF)
SELECT DISTINCT ON (four) four,hundred
FROM tenk1 WHERE four = 0 ORDER BY 1,2;
- QUERY PLAN
------------------------------------------------
- Limit
- -> Index Scan using tenk1_hundred on tenk1
- Filter: (four = 0)
-(3 rows)
+ QUERY PLAN
+-----------------------------------------------------
+ Result
+ -> Limit
+ -> Index Scan using tenk1_hundred on tenk1
+ Filter: (four = 0)
+(4 rows)
--
-- Test the planner's ability to reorder the distinctClause Pathkeys to match
diff --git a/src/test/regress/expected/select_parallel.out
b/src/test/regress/expected/select_parallel.out
index 933921d1860..a3d6f3d4576 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -753,20 +753,18 @@ end;
$$ language plpgsql PARALLEL SAFE;
explain (costs off, verbose)
select ten, sp_simple_func(ten) from tenk1 where ten < 100 order by ten;
- QUERY PLAN
------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------
Gather Merge
- Output: ten, (sp_simple_func(ten))
+ Output: ten, sp_simple_func(ten)
Workers Planned: 4
- -> Result
- Output: ten, sp_simple_func(ten)
- -> Sort
+ -> Sort
+ Output: ten
+ Sort Key: tenk1.ten
+ -> Parallel Seq Scan on public.tenk1
Output: ten
- Sort Key: tenk1.ten
- -> Parallel Seq Scan on public.tenk1
- Output: ten
- Filter: (tenk1.ten < 100)
-(11 rows)
+ Filter: (tenk1.ten < 100)
+(9 rows)
drop function sp_simple_func(integer);
-- test handling of SRFs in targetlist (bug in 10.0)
@@ -1261,18 +1259,16 @@ SELECT generate_series(1, two), array(select
generate_series(1, two))
-> Gather Merge
Output: tenk1.two, tenk1.tenthous
Workers Planned: 4
- -> Result
- Output: tenk1.two, tenk1.tenthous
- -> Sort
+ -> Sort
+ Output: tenk1.tenthous, tenk1.two
+ Sort Key: tenk1.tenthous
+ -> Parallel Seq Scan on public.tenk1
Output: tenk1.tenthous, tenk1.two
- Sort Key: tenk1.tenthous
- -> Parallel Seq Scan on public.tenk1
- Output: tenk1.tenthous, tenk1.two
SubPlan array_1
-> ProjectSet
Output: generate_series(1, tenk1.two)
-> Result
-(16 rows)
+(14 rows)
-- must disallow pushing sort below gather when pathkey contains an SRF
EXPLAIN (VERBOSE, COSTS OFF)
diff --git a/src/test/regress/expected/tsrf.out
b/src/test/regress/expected/tsrf.out
index c4f7b187f5b..a0d295859ed 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -459,12 +459,12 @@ reset enable_hashagg;
-- case with degenerate ORDER BY
explain (verbose, costs off)
select 'foo' as f, generate_series(1,2) as g from few order by 1;
- QUERY PLAN
-----------------------------------------------
+ QUERY PLAN
+------------------------------------------------
ProjectSet
- Output: 'foo'::text, generate_series(1, 2)
+ Output: ('foo'::text), generate_series(1, 2)
-> Seq Scan on public.few
- Output: id, dataa, datab
+ Output: 'foo'::text
(4 rows)
select 'foo' as f, generate_series(1,2) as g from few order by 1;
--
2.53.0