From 980f234baab75ccb11a7b1ad31633172e76a73c9 Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Tue, 11 Aug 2026 19:58:54 +0900 Subject: [PATCH] Clear the RPR reluctant flag when min equals max Reluctance decides nothing once min == max, yet the merge and multiplication rewrites decline a reluctant node, so (A{2}?){3} stayed nested where (A{2}){3} collapses to A{6}. Clear the flag in optimizeRPRPattern(), before and after each node's own rewrites. Clearing it is safe because the matcher reads RPR_ELEM_RELUCTANT only where the quantifier still has a choice: on a skip path, which needs min == 0, or under canLoop && canExit, which needs count < max and count >= min. A bound of zero is rejected during parsing, so min == max means min >= 1 and none of those reads apply. The planner is where clearing has an effect, in more places than the merge and multiplication rewrites that motivate it. rprPatternEqual() compares reluctance, which makes the group, ALT, prefix/suffix and deduplication rewrites treat a reluctant node as unequal to its greedy twin, so (A{2}? | A{2}) now deduplicates to A{2}. The absorbability analysis reads the flag too, on a group's BEGIN and with no bound test, so (A+ B){2}? is now marked absorbable. In every case the fixed-count reluctant spelling ends up with the plan its greedy twin already had, which is what makes the results identical. That makes the {1} spelling explain.c emitted for a reluctant {1,1} unreachable; drop it and assert the invariant. tryUnwrapGroup() no longer needs to test the child's flag either -- a {1,1} child has been through optimizeRPRPattern() already, and the next statement overwrites the flag regardless. Query results do not change. Switch the outer-reluctant multiply bypass from (A{2,3}){2,3}?, which the greedy spelling does not multiply either, to (A+){2,4}?, whose greedy twin collapses to a{2,}. Correct a test header that credited A{2}? with shortest-match behaviour, add the brace spellings the optimizer normalizes away -- including the reluctant {1,1} that the new assertion guards -- and adjust the Pattern: lines the normalization shifts. --- src/backend/commands/explain.c | 5 +- src/backend/executor/README.rpr | 19 ++++- src/backend/optimizer/plan/rpr.c | 37 ++++++--- src/test/regress/expected/rpr_base.out | 91 +++++++++++++++++++---- src/test/regress/expected/rpr_explain.out | 80 ++++++++++++++++++++ src/test/regress/sql/rpr_base.sql | 47 +++++++++--- src/test/regress/sql/rpr_explain.sql | 46 ++++++++++++ 7 files changed, 285 insertions(+), 40 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index fa30b5ca0ad..0c82a7d57ce 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -2928,11 +2928,10 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem) else if (elem->min != 1 || elem->max != 1) appendStringInfo(buf, "{%d,%d}", elem->min, elem->max); + /* A fixed count is normalized to greedy, so '?' cannot be read as {0,1} */ if (RPRElemIsReluctant(elem)) { - if (elem->min == 1 && elem->max == 1) - appendStringInfoString(buf, "{1}"); /* make reluctant ? - * unambiguous */ + Assert(elem->min != elem->max); appendStringInfoChar(buf, '?'); } diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr index 3a965fc01f8..c862c28a1a0 100644 --- a/src/backend/executor/README.rpr +++ b/src/backend/executor/README.rpr @@ -251,6 +251,19 @@ never different rows. (h) Single-child unwrap SEQ(A) -> A, (A){1,1} -> A + (i) Reluctance normalization: a fixed count leaves reluctance nothing + to decide, so it is cleared wherever min == max + A{2}? -> A{2}, (A B){2}? -> (A B){2} + This runs before and after each node's own rewrites. (b) and (g) + decline a reluctant node outright, and (c), (d), (e) and (f) compare + nodes with rprPatternEqual(), which treats a reluctant node as unequal + to its greedy twin. Without the normalization (A{2}?){3} would stay + nested where (A{2}){3} collapses to A{6}, and (A{2}? | A{2}) would + keep both branches. The absorbability analysis of IV-5 reads the flag + as well, on a group's BEGIN and with no bound test, so a fixed-count + reluctant group becomes absorbable exactly where its greedy spelling + already was. + IV-4. Phase 4: NFA Element Array Generation Transforms the optimized parse tree into a flat array of RPRPatternElement. @@ -416,8 +429,10 @@ Example: PATTERN ((B C)* | A) -- GROUP + ALT combined IV-4a. Reluctant Flag (RPR_ELEM_RELUCTANT) -The reluctant flag is set during Phase 4 (fillRPRPattern) when the parse -tree node has reluctant == true. It reverses the priority of quantifier +The reluctant flag is set during Phase 4 (fillRPRPattern) from the parse +tree node's reluctant field. Phase 1 (i) has already cleared that field +wherever min == max, so A{2}? and (A B){2}? reach here as the plain +A{2} and (A B){2}. The flag reverses the priority of quantifier expansion at runtime: Greedy (default): try loop-back first, then exit (prefer longer match) diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c index bbcf6734967..8b26bd95ff7 100644 --- a/src/backend/optimizer/plan/rpr.c +++ b/src/backend/optimizer/plan/rpr.c @@ -252,9 +252,12 @@ flattenSeqChildren(List *children) { RPRPatternNode *opt = optimizeRPRPattern(child); - /* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */ + /* + * GROUP{1,1} should have been unwrapped by optimizeGroupPattern; + * tryUnwrapGroup() does so regardless of reluctance. + */ Assert(!(opt->nodeType == RPR_PATTERN_GROUP && - opt->min == 1 && opt->max == 1 && opt->reluctant == false)); + opt->min == 1 && opt->max == 1)); if (opt->nodeType == RPR_PATTERN_SEQ) { @@ -1006,7 +1009,7 @@ tryUnwrapGroup(RPRPatternNode *pattern) * the child and unwrap. E.g., (A)?? -> A??, (A)+? -> A+? */ if (child->nodeType == RPR_PATTERN_VAR && - child->min == 1 && child->max == 1 && child->reluctant == false) + child->min == 1 && child->max == 1) { child->min = pattern->min; child->max = pattern->max; @@ -1058,25 +1061,41 @@ optimizeGroupPattern(RPRPatternNode *pattern) static RPRPatternNode * optimizeRPRPattern(RPRPatternNode *pattern) { + RPRPatternNode *result = pattern; + /* Pattern nodes from parser are never NULL */ Assert(pattern != NULL); check_stack_depth(); + /* + * A fixed count leaves reluctance nothing to decide. Drop it here: the + * merge and multiplication rewrites below decline a reluctant node, so + * {n,n}? would otherwise miss what {n,n} gets. + */ + if (pattern->min == pattern->max) + pattern->reluctant = false; + switch (pattern->nodeType) { case RPR_PATTERN_VAR: - return pattern; + break; case RPR_PATTERN_SEQ: - return optimizeSeqPattern(pattern); + result = optimizeSeqPattern(pattern); + break; case RPR_PATTERN_ALT: - return optimizeAltPattern(pattern); + result = optimizeAltPattern(pattern); + break; case RPR_PATTERN_GROUP: - return optimizeGroupPattern(pattern); + result = optimizeGroupPattern(pattern); + break; } - pg_unreachable(); - return pattern; + /* Again: a rewrite may have produced a fixed count of its own */ + if (result->min == result->max) + result->reluctant = false; + + return result; } /* diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out index 2aa2c8b0992..2bc272298d0 100644 --- a/src/test/regress/expected/rpr_base.out +++ b/src/test/regress/expected/rpr_base.out @@ -1316,8 +1316,9 @@ WINDOW w AS ( 1 (3 rows) --- {n}? (exactly n, reluctant) --- Reluctant quantifier: prefer shortest match +-- {n}? (exactly n): min == max, so the reluctant flag is cleared and the +-- plan is indistinguishable from A{2}. This pins the normalization, not +-- shortest-match behaviour. SELECT COUNT(*) OVER w FROM rpr_reluctant WINDOW w AS ( @@ -5053,8 +5054,9 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested --- a reluctant quantifier on a GROUP is not subject to multiplication +-- Quantifier multiply: (((A B){2}?){3}) -> (a b){6} +-- {2}? has a fixed count, so reluctance is normalized away and the +-- multiplication that (((A B){2}){3}) gets applies here too EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -5063,7 +5065,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ------------------------------------------------------------------------------- WindowAgg Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) - Pattern: ((a b){2}?){3} + Pattern: (a b){6} -> Sort Sort Key: id -> Seq Scan on rpr_plan @@ -5681,23 +5683,38 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (6 rows) -- Reluctant optimization bypass: quantifier multiply (outer reluctant) --- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6}) +-- (A+){2,4}? stays nested where the greedy (A+){2,4} multiplies to a{2,} EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN ((A{2}){3}?) DEFINE A AS val > 0); + PATTERN ((A+){2,4}?) DEFINE A AS val > 0); QUERY PLAN ------------------------------------------------------------------------------- WindowAgg Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) - Pattern: (a{2}){3}? + Pattern: (a+){2,4}? -> Sort Sort Key: id -> Seq Scan on rpr_plan (6 rows) -- Reluctant optimization bypass: quantifier multiply (inner reluctant) --- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6}) +-- (A{2,3}?){3} stays as (a{2,3}?){3} (greedy (A{2,3}){3} merges to a{6,9}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}?){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2,3}?){3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Fixed count normalizes away: (A{2}?){3} -> a{6}, same as (A{2}){3} EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -5706,7 +5723,54 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ------------------------------------------------------------------------------- WindowAgg Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) - Pattern: (a{2}?){3} + Pattern: a{6} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- {0,} is the only brace spelling that reaches an unbounded min 0 +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Bare {1} and {1,1} carry no quantifier, on a VAR or on a GROUP +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1} B{1,1} (C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- A reluctant {1,1} normalizes to a bare variable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,1}? (B C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c -> Sort Sort Key: id -> Seq Scan on rpr_plan @@ -6222,10 +6286,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Reluctant {1}? quantifier deparse --- A{1}? is a reluctant {1,1} quantifier. The deparse code must --- output "{1}" explicitly to disambiguate from a bare "?" quantifier --- (which would mean {0,1}). +-- Reluctant {1}? quantifier: min == max, so the plan normalizes it away EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM rpr_plan WINDOW w AS ( @@ -6238,7 +6299,7 @@ WINDOW w AS ( -------------------------------------------------------------------------------- WindowAgg Window: w AS (ORDER BY val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) - Pattern: a{1}? b + Pattern: a b -> Sort Sort Key: val -> Seq Scan on rpr_plan diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out index 112e483d55b..3d03351daa0 100644 --- a/src/test/regress/expected/rpr_explain.out +++ b/src/test/regress/expected/rpr_explain.out @@ -1510,6 +1510,86 @@ WINDOW w AS ( -> Function Scan on generate_series s (actual rows=50.00 loops=1) (9 rows) +-- A fixed count leaves reluctance nothing to decide, so (A+ B){2}? is +-- normalized to (A+ B){2} and absorbs where the unbounded (A+ B)+? above +-- does not. The two spellings below have to report the same marker and the +-- same absorbed count as each other; that is what the normalization buys. +CREATE VIEW rpr_ev_ctx_absorb_fixed_greedy AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixed_greedy'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a+ b){2}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b){2} + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 111 total, 0 merged + NFA Contexts: 3 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +CREATE VIEW rpr_ev_ctx_absorb_fixed_reluctant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}?) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixed_reluctant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a+ b){2}?) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}?) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b){2} + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 111 total, 0 merged + NFA Contexts: 3 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + -- Alternation, non-absorbable branch match survives absorption: A+ B | C -- The dominating A+ run absorbs redundant contexts, but the recorded C matches -- are not absorbable, so they survive (2 matched, not 0) diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql index 28859538a48..5a90ab38d4f 100644 --- a/src/test/regress/sql/rpr_base.sql +++ b/src/test/regress/sql/rpr_base.sql @@ -940,8 +940,9 @@ WINDOW w AS ( DEFINE A AS val > 0 ); --- {n}? (exactly n, reluctant) --- Reluctant quantifier: prefer shortest match +-- {n}? (exactly n): min == max, so the reluctant flag is cleared and the +-- plan is indistinguishable from A{2}. This pins the normalization, not +-- shortest-match behaviour. SELECT COUNT(*) OVER w FROM rpr_reluctant WINDOW w AS ( @@ -3223,8 +3224,9 @@ SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A{2}){3}) DEFINE A AS val > 0); --- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested --- a reluctant quantifier on a GROUP is not subject to multiplication +-- Quantifier multiply: (((A B){2}?){3}) -> (a b){6} +-- {2}? has a fixed count, so reluctance is normalized away and the +-- multiplication that (((A B){2}){3}) gets applies here too EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -3496,19 +3498,45 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B)+? (A B)) DEFINE A AS val <= 50, B AS val > 50); -- Reluctant optimization bypass: quantifier multiply (outer reluctant) --- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6}) +-- (A+){2,4}? stays nested where the greedy (A+){2,4} multiplies to a{2,} EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN ((A{2}){3}?) DEFINE A AS val > 0); + PATTERN ((A+){2,4}?) DEFINE A AS val > 0); -- Reluctant optimization bypass: quantifier multiply (inner reluctant) --- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6}) +-- (A{2,3}?){3} stays as (a{2,3}?){3} (greedy (A{2,3}){3} merges to a{6,9}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}?){3}) DEFINE A AS val > 0); + +-- Fixed count normalizes away: (A{2}?){3} -> a{6}, same as (A{2}){3} EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A{2}?){3}) DEFINE A AS val > 0); +-- {0,} is the only brace spelling that reaches an unbounded min 0 +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,}) DEFINE A AS val > 0); + +-- Bare {1} and {1,1} carry no quantifier, on a VAR or on a GROUP +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1} B{1,1} (C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- A reluctant {1,1} normalizes to a bare variable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,1}? (B C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + -- Reluctant optimization bypass: PREFIX merge -- A B (A B)+? stays separate (greedy merges to (a b){2,}) EXPLAIN (COSTS OFF) @@ -3732,10 +3760,7 @@ SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); --- Reluctant {1}? quantifier deparse --- A{1}? is a reluctant {1,1} quantifier. The deparse code must --- output "{1}" explicitly to disambiguate from a bare "?" quantifier --- (which would mean {0,1}). +-- Reluctant {1}? quantifier: min == max, so the plan normalizes it away EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM rpr_plan WINDOW w AS ( diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql index f28c0cdacb1..b4712029a88 100644 --- a/src/test/regress/sql/rpr_explain.sql +++ b/src/test/regress/sql/rpr_explain.sql @@ -940,6 +940,52 @@ WINDOW w AS ( DEFINE A AS v % 10 NOT IN (0, 9), B AS v % 10 = 9, C AS v % 10 = 0 );'); +-- A fixed count leaves reluctance nothing to decide, so (A+ B){2}? is +-- normalized to (A+ B){2} and absorbs where the unbounded (A+ B)+? above +-- does not. The two spellings below have to report the same marker and the +-- same absorbed count as each other; that is what the normalization buys. +CREATE VIEW rpr_ev_ctx_absorb_fixed_greedy AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixed_greedy'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +CREATE VIEW rpr_ev_ctx_absorb_fixed_reluctant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}?) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixed_reluctant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ B){2}?) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + -- Alternation, non-absorbable branch match survives absorption: A+ B | C -- The dominating A+ run absorbs redundant contexts, but the recorded C matches -- are not absorbable, so they survive (2 matched, not 0)