From 51a64ae43a938a461392f6352e6c8ad7a2f70e9c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri, 10 Jul 2026 23:29:01 +0300
Subject: [PATCH v3 5/5] Avoid redundant re-evaluation of JSON_TABLE nested
 paths

86ab7f4c721d makes JSON_TABLE with a NESTED PATH re-ran the nested path's
jsonpath expression several times for each parent row.  That makes such
queries significantly slower as the nested arrays grew, even without a PLAN
clause.

Two leftovers from the plan/join executor rework were responsible.
JsonTablePlanScanNextRow() still reset and advanced the nested plan
itself, although JsonTablePlanNextRow() now does that; and
JsonTableResetNestedPlan() eagerly called JsonTableResetRowPattern()
(which evaluates the path) in addition to setting the reset flag that
makes JsonTablePlanNextRow() evaluate it again.  Together these caused
the nested path to be evaluated multiple times per parent row.

Reduce JsonTablePlanScanNextRow() to advancing its own row pattern
iterator, and have JsonTableResetNestedPlan() only reset the transient
scan state (so a not-yet-advanced sibling still reads as NULL) while
deferring the actual path evaluation to the reset flag.  The nested path
is now evaluated exactly once per parent row, as before the PLAN clause
feature; results are unchanged and are covered by the existing tests.

Reported-by: Thom Brown <thom@linux.com>
Discussion: https://postgr.es/m/CAA-aLv5U94KD4C%2BLhAPYcCeGvs1xBMngcS5oEkZHN9YWwXUHsA%40mail.gmail.com
---
 src/backend/utils/adt/jsonpath_exec.c | 48 ++++++---------------------
 1 file changed, 11 insertions(+), 37 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index b69c87d8587..6550d85a2f8 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -4699,16 +4699,12 @@ JsonTablePlanNextRow(JsonTablePlanState *planstate)
 }
 
 /*
- * Fetch next row from a JsonTablePlan's path evaluation result and from
- * any child nested path(s).
+ * Advance a JsonTablePlan's path scan to its next row pattern match.
  *
- * Returns true if any of the paths (this or the nested) has more rows to
- * return.
- *
- * By fetching the nested path(s)'s rows based on the parent row at each
- * level, this essentially joins the rows of different levels.  If a nested
- * path at a given level has no matching rows, the columns of that level will
- * compute to NULL, making it an OUTER join.
+ * This only moves this plan's own row pattern iterator forward and makes the
+ * matched item the current row; driving and joining of any nested plan is the
+ * responsibility of JsonTablePlanNextRow().  Returns false when this scan's
+ * row pattern matches are exhausted.
  */
 static bool
 JsonTablePlanScanNextRow(JsonTablePlanState *planstate)
@@ -4716,16 +4712,6 @@ JsonTablePlanScanNextRow(JsonTablePlanState *planstate)
 	JsonbValue *jbv;
 	MemoryContext oldcxt;
 
-	/*
-	 * If planstate already has an active row and there is a nested plan,
-	 * check if it has an active row to join with the former.
-	 */
-	if (!planstate->current.isnull)
-	{
-		if (planstate->nested && JsonTablePlanNextRow(planstate->nested))
-			return true;
-	}
-
 	/* Fetch new row from the list of found values to set as active. */
 	jbv = JsonValueListNext(&planstate->iter);
 
@@ -4749,21 +4735,6 @@ JsonTablePlanScanNextRow(JsonTablePlanState *planstate)
 	/* Next row! */
 	planstate->ordinal++;
 
-	/* Process nested plan(s), if any. */
-	if (planstate->nested)
-	{
-		/* Re-evaluate the nested path using the above parent row. */
-		JsonTableResetNestedPlan(planstate->nested);
-
-		/*
-		 * Now fetch the nested plan's current row to be joined against the
-		 * parent row.  Any further nested plans' paths will be re-evaluated
-		 * recursively, level at a time, after setting each nested plan's
-		 * current row.
-		 */
-		(void) JsonTablePlanNextRow(planstate->nested);
-	}
-
 	/* There are more rows. */
 	return true;
 }
@@ -4788,11 +4759,14 @@ JsonTableResetNestedPlan(JsonTablePlanState *planstate)
 			JsonTableResetNestedPlan(planstate->nested);
 
 		/*
-		 * If this plan itself has a child nested plan, it will be reset when
-		 * the caller calls JsonTablePlanNextRow() on this plan.
+		 * Reset this plan's transient scan state so that its columns read as
+		 * NULL until it is actually advanced.  Re-evaluating the path against
+		 * the new parent row is deferred (see the reset flag) until the plan
+		 * is advanced by JsonTablePlanNextRow(), so that the path is not
+		 * evaluated more than once per parent row.
 		 */
 		if (!parent->current.isnull)
-			JsonTableResetRowPattern(planstate, parent->current.value);
+			JsonTableRescan(planstate);
 	}
 	else if (IsA(planstate->plan, JsonTableSiblingJoin))
 	{
-- 
2.50.1 (Apple Git-155)

