From 8b4dc277fff2bf568240b5a1bfe52c8f0d7141ad Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Fri, 10 Jul 2026 23:29:01 +0300 Subject: [PATCH v4 5/6] 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 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 74b6a793c90..9a37e6fe663 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -4698,16 +4698,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) @@ -4715,16 +4711,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); @@ -4748,21 +4734,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; } @@ -4787,11 +4758,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)