From 33f25c21199e7f16bef1e93a3c3caac961f04e5f Mon Sep 17 00:00:00 2001 From: "Zizhuan Liu(X-MAN)" <44973863@qq.com> Date: Mon, 12 Oct 2026 22:11:56 +0800 Subject: [PATCH v6] Fix RelabelType handling in statistics lookup Regarding examine_variable() in src/backend/utils/adt/selfuncs.c: 1. Handling adjacent RelabelType nodes after stripping PHVs: PATCH[1] added PHV stripping for node, followed by the following loop on the resulting basenode: while (IsA(basenode, RelabelType)) basenode = (Node *) ((RelabelType *) basenode)->arg; This loop strips only consecutive RelabelType nodes at the top level. It does not handle adjacent RelabelType nodes that may arise deeper in the expression tree. Since stripping PHVs, as introduced by PATCH[1], can bring previously separated RelabelType nodes into adjacency, I think it would be better to strip these adjacent nodes at the point where this adjacency can arise. This keeps the creation and removal of adjacent RelabelType nodes together as a single, cohesive operation, without requiring a heavyweight function. I therefore chose to call the new helper function strip_all_adjacency_relabeltypes() from within strip_all_phvs_mutator(). (Sorry, my previous version of strip_all_adjacency_relabeltypes() did not actually return the stripped expression.) 2. Preserving the previous lookup behavior: Before PATCH[1], there was a symmetric attempt to strip the topmost RelabelType from the input node, index expressions, and extended statistics expressions before comparing them. The relevant code is as follows: --for node if (IsA(node, RelabelType)) basenode = (Node *) ((RelabelType *) node)->arg; else basenode = node; --for index's expr if (indexkey && IsA(indexkey, RelabelType)) indexkey = (Node *) ((RelabelType *) indexkey)->arg; --for extended statistics if (expr && IsA(expr, RelabelType)) expr = (Node *) ((RelabelType *) expr)->arg; The original behavior was to strip the topmost RelabelType before comparing expressions with equal(). However, this is not entirely satisfactory. Ideally, we should use statistics associated with expressions that match exactly according to equal(). Stripping the topmost RelabelType before comparison should be a fallback rather than the preferred approach. Therefore, the new PATCH first compares the input expression, index expressions, and extended statistics expressions as they are, without stripping their topmost RelabelType nodes. If no matching statistics are found, it retries the lookup under certain conditions, symmetrically stripping the topmost RelabelType from the input expression and the candidate expressions before comparing them. This preserves the behavior that existed before PATCH[1] as a fallback, maintaining compatibility with the previous lookup logic. (The normalization performed by strip_all_phvs_mutator(), following logic similar to applyRelabelType(), is a separate matter and remains appropriate. It normalizes adjacent RelabelType nodes that may arise when PHVs are stripped, while preserving the semantics of the outermost relabeling. This is consistent with the normalization already applied to index expressions and extended statistics expressions.) Feedback is welcome. [1] https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=7e9f852a79fe19d4d0f18aabc32a620797fb676e [1] https://www.postgresql.org/message-id/flat/E1va3GH-003FBe-35%40gemulon.postgresql.org discussion: https://www.postgresql.org/message-id/flat/CACJufxGLCiyhM+P0gxesg2x--PTrMY3PszqSqOq_H4QS_oq3Jg@mail.gmail.com commitfest: https://commitfest.postgresql.org/patch/6397/ discussion: https://www.postgresql.org/message-id/flat/E1va3GH-003FBe-35%40gemulon.postgresql.org --- src/backend/utils/adt/selfuncs.c | 190 +++++++++++++++--- .../regress/expected/collate.icu.utf8.out | 186 +++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 132 ++++++++++++ 3 files changed, 484 insertions(+), 24 deletions(-) diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index 5ee37759..448e9f27 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -242,6 +242,7 @@ static double convert_timevalue_to_scalar(Datum value, Oid typid, bool *failure); static Node *strip_all_phvs_deep(PlannerInfo *root, Node *node); static bool contain_placeholder_walker(Node *node, void *context); +static Node *strip_all_adjacency_relabeltypes(Node *node); static Node *strip_all_phvs_mutator(Node *node, void *context); static void examine_simple_variable(PlannerInfo *root, Var *var, VariableStatData *vardata); @@ -5622,8 +5623,10 @@ ReleaseDummy(HeapTuple tuple) * varRelid: see specs for restriction selectivity functions * * Outputs: *vardata is filled as follows: - * var: the input expression (with any phvs or binary relabeling stripped, - * if it is or contains a variable; but otherwise unchanged) + * var: the input expression (with PHVs stripped and adjacent + * RelabelTypes normalized if it is or contains a variable. + * The topmost RelabelType may also be stripped on retry. + * Otherwise, the expression is unchanged.) * rel: RelOptInfo for relation containing variable; NULL if expression * contains no Vars (NOTE this could point to a RelOptInfo of a * subquery, not one in the current query). @@ -5654,6 +5657,10 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, Relids varnos; Relids basevarnos; RelOptInfo *onerel; + bool try_again = false; + bool basenode_is_topmost_relabeltype = false; + bool indexkey_is_topmost_relabeltype = false; + bool statexpr_is_topmost_relabeltype = false; /* Make sure we don't return dangling pointers in vardata */ MemSet(vardata, 0, sizeof(VariableStatData)); @@ -5670,13 +5677,22 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, */ basenode = strip_all_phvs_deep(root, node); - /* - * Look inside any binary-compatible relabeling. We need to handle nested - * RelabelType nodes here, because the prior stripping of PlaceHolderVars - * may have brought separate RelabelTypes into adjacency. - */ - while (IsA(basenode, RelabelType)) - basenode = (Node *) ((RelabelType *) basenode)->arg; +retry: + if (!try_again) + { + basenode_is_topmost_relabeltype = IsA(basenode, RelabelType); + } + else + { + Oid vartype = vardata->vartype; + + MemSet(vardata, 0, sizeof(VariableStatData)); + vardata->vartype = vartype; + + /* Strip the topmost RelabelType before comparing when retrying. */ + if (IsA(basenode, RelabelType)) + basenode = (Node *) ((RelabelType *) basenode)->arg; + } /* Fast path for a simple Var */ if (IsA(basenode, Var) && @@ -5685,7 +5701,12 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, Var *var = (Var *) basenode; /* Set up result fields other than the stats tuple */ - vardata->var = basenode; /* return Var without phvs or relabeling */ + + /* + * return Var without phvs or adjacent relabeling, or strip the + * topmost RelabelType when retrying. + */ + vardata->var = basenode; vardata->rel = find_base_rel(root, var->varno); vardata->atttype = var->vartype; vardata->atttypmod = var->vartypmod; @@ -5722,7 +5743,12 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, { onerel = find_base_rel(root, relid); vardata->rel = onerel; - node = basenode; /* strip any phvs or relabeling */ + + /* + * strip any phvs or adjacent relabeling, or strip the topmost + * RelabelType when retrying. + */ + node = basenode; } /* else treat it as a constant */ } @@ -5733,13 +5759,23 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, { /* treat it as a variable of a join relation */ vardata->rel = find_join_rel(root, varnos); - node = basenode; /* strip any phvs or relabeling */ + + /* + * strip any phvs or adjacent relabeling, or strip the topmost + * RelabelType when retrying. + */ + node = basenode; } else if (bms_is_member(varRelid, varnos)) { /* ignore the vars belonging to other relations */ vardata->rel = find_base_rel(root, varRelid); - node = basenode; /* strip any phvs or relabeling */ + + /* + * strip any phvs or adjacent relabeling, or strip the topmost + * RelabelType when retrying. + */ + node = basenode; /* note: no point in expressional-index search here */ } /* else treat it as a constant */ @@ -5776,7 +5812,7 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, * extended statistics. So strip them out first. */ if (bms_overlap(varnos, root->outer_join_rels)) - node = remove_nulling_relids(node, root->outer_join_rels, NULL); + basenode = remove_nulling_relids(basenode, root->outer_join_rels, NULL); foreach(ilist, onerel->indexlist) { @@ -5784,6 +5820,13 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, ListCell *indexpr_item; int pos; + if (try_again && + !(basenode_is_topmost_relabeltype || + indexkey_is_topmost_relabeltype)) + { + break; + } + indexpr_item = list_head(index->indexprs); if (indexpr_item == NULL) continue; /* no expressions here... */ @@ -5797,9 +5840,21 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, if (indexpr_item == NULL) elog(ERROR, "too few entries in indexprs list"); indexkey = (Node *) lfirst(indexpr_item); - if (indexkey && IsA(indexkey, RelabelType)) - indexkey = (Node *) ((RelabelType *) indexkey)->arg; - if (equal(node, indexkey)) + if (!try_again) + { + indexkey_is_topmost_relabeltype = indexkey_is_topmost_relabeltype || + (indexkey && IsA(indexkey, RelabelType)); + } + else + { + /* + * Strip the topmost RelabelType before comparing when + * retrying. + */ + if (indexkey && IsA(indexkey, RelabelType)) + indexkey = (Node *) ((RelabelType *) indexkey)->arg; + } + if (equal(basenode, indexkey)) { /* * Found a match ... is it a unique index? Tests here @@ -5901,6 +5956,13 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, ListCell *expr_item; int pos; + if (try_again && + !(basenode_is_topmost_relabeltype || + statexpr_is_topmost_relabeltype)) + { + break; + } + /* * Stop once we've found statistics for the expression (either * from extended stats, or for an index in the preceding loop). @@ -5923,12 +5985,22 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, Assert(expr); - /* strip RelabelType before comparing it */ - if (expr && IsA(expr, RelabelType)) - expr = (Node *) ((RelabelType *) expr)->arg; - + if (!try_again) + { + statexpr_is_topmost_relabeltype = statexpr_is_topmost_relabeltype || + (expr && IsA(expr, RelabelType)); + } + else + { + /* + * Strip the topmost RelabelType before comparing when + * retrying. + */ + if (expr && IsA(expr, RelabelType)) + expr = (Node *) ((RelabelType *) expr)->arg; + } /* found a match, see if we can extract pg_statistic row */ - if (equal(node, expr)) + if (equal(basenode, expr)) { /* * XXX Not sure if we should cache the tuple somewhere. @@ -5974,6 +6046,20 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, } bms_free(varnos); + + /* + * If the initial lookup did not find statistics, retry without the + * topmost RelabelType to preserve the previous lookup behavior. + */ + if (!vardata->statsTuple && !try_again && + (basenode_is_topmost_relabeltype || + indexkey_is_topmost_relabeltype || + statexpr_is_topmost_relabeltype)) + { + try_again = true; + + goto retry; + } } /* @@ -6013,6 +6099,52 @@ contain_placeholder_walker(Node *node, void *context) return expression_tree_walker(node, contain_placeholder_walker, context); } +/* + * strip_all_adjacency_relabeltypes + * deeply strip all adjacent RelabelTypes. + * + * Borrow the relevant logic from applyRelabelType(). Remove + * stacked inner RelabelTypes, retaining the outer one unless + * the underlying expression already has the same result type, + * typmod, and collation. This ensures that redundant relabeling + * is removed while preserving the outer expression's semantics. + */ +static Node * +strip_all_adjacency_relabeltypes(Node *node) +{ + if (IsA(node, RelabelType)) + { + RelabelType *relabel = (RelabelType *) node; + Node *arg = (Node *) relabel->arg; + bool strip_adjacent_relabeltypes = false; + + /* + * If we find stacked RelabelTypes (eg, from foo::int::oid) we can + * discard all but the top one, and must do so to ensure that + * semantically equivalent expressions are equal(). + */ + while (arg && IsA(arg, RelabelType)) + { + strip_adjacent_relabeltypes = true; + arg = (Node *) ((RelabelType *) arg)->arg; + } + + if (strip_adjacent_relabeltypes) + { + if (exprType(arg) == relabel->resulttype && + exprTypmod(arg) == relabel->resulttypmod && + exprCollation(arg) == relabel->resultcollid) + { + node = arg; + } + else + relabel->arg = (Expr *) arg; + } + } + + return node; +} + /* * strip_all_phvs_mutator * Mutator to deeply strip all PlaceHolderVars @@ -6027,10 +6159,20 @@ strip_all_phvs_mutator(Node *node, void *context) /* Strip it and recurse into its contained expression */ PlaceHolderVar *phv = (PlaceHolderVar *) node; - return strip_all_phvs_mutator((Node *) phv->phexpr, context); + node = strip_all_phvs_mutator((Node *) phv->phexpr, context); } + else + node = expression_tree_mutator(node, strip_all_phvs_mutator, context); - return expression_tree_mutator(node, strip_all_phvs_mutator, context); + /* + * Look inside binary-compatible relabeling. Stripping PlaceHolderVars + * above may have brought previously separated RelabelType nodes into + * adjacency, either at the top level or deeper in the expression tree. + * Normalize these cases before statistics lookup so that equal() and + * other structural comparisons can match semantically equivalent + * expressions. + */ + return strip_all_adjacency_relabeltypes(node); } /* diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 69b2f510..a7f6cf6d 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -11,6 +11,25 @@ SELECT getdatabaseencoding() <> 'UTF8' OR SET client_encoding TO UTF8; CREATE SCHEMA collate_tests; SET search_path = collate_tests; +create function check_estimated_rows(text) returns table (estimated int, actual int) +language plpgsql as +$$ +declare + ln text; + tmp text[]; + first_row bool := true; +begin + for ln in + execute format('explain analyze %s', $1) + loop + if first_row then + first_row := false; + tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)'); + return query select tmp[1]::int, tmp[2]::int; + end if; + end loop; +end; +$$; CREATE TABLE collate_test1 ( a int, b text COLLATE "en-x-icu" NOT NULL @@ -1469,6 +1488,35 @@ CREATE COLLATION case_sensitive (provider = icu, locale = ''); NOTICE: using standard form "und" for ICU locale "" CREATE COLLATION case_insensitive (provider = icu, locale = '@colStrength=secondary', deterministic = false); NOTICE: using standard form "und-u-ks-level2" for ICU locale "@colStrength=secondary" +CREATE TABLE test_stats_ext_coll (a text, b text, c int); +INSERT INTO test_stats_ext_coll SELECT chr(65 + g % 52) FROM generate_series(1, 500) g; +CREATE STATISTICS test_stats_ext_coll_stx ON (a COLLATE case_insensitive) FROM test_stats_ext_coll; +ANALYZE test_stats_ext_coll; +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE "C" $$); + estimated | actual +-----------+-------- + 52 | 52 +(1 row) + +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE "POSIX" $$); + estimated | actual +-----------+-------- + 52 | 52 +(1 row) + +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE case_insensitive $$); + estimated | actual +-----------+-------- + 32 | 32 +(1 row) + +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a $$); + estimated | actual +-----------+-------- + 52 | 52 +(1 row) + +DROP TABLE test_stats_ext_coll; SELECT 'abc' <= 'ABC' COLLATE case_sensitive, 'abc' >= 'ABC' COLLATE case_sensitive; ?column? | ?column? ----------+---------- @@ -3954,6 +4002,144 @@ LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLAT... ^ DETAIL: "C" versus "case_insensitive" DROP DOMAIN d1, d2; +-- Checks the estimated and actual row counts for the specified pattern +-- in the EXPLAIN ANALYZE output of the given SQL query. +CREATE FUNCTION check_estimated_rows_by_pattern(in_pattern text, in_sql text) RETURNS TABLE (estimated text, actual text) +LANGUAGE PLPGSQL AS +$$ +DECLARE + ln text; + tmp text[]; + tmp_pattern text; +BEGIN + FOR ln IN execute FORMAT('EXPLAIN ANALYZE %s', in_sql) + LOOP + tmp_pattern := FORMAT('(%s)', in_pattern); + tmp := REGEXP_MATCH(ln, tmp_pattern); + IF tmp IS NOT NULL THEN + tmp := REGEXP_MATCH(ln, 'rows=(\d*) .* rows=([0-9.]+)'); + RETURN QUERY SELECT tmp[1]::text, tmp[2]::text; + END IF; + END LOOP; +END; +$$; +-- Verify that statistics can be found on retry when the initial +-- lookup fails due to a collation mismatch. +-- Strips the topmost RelabelType from the WHERE expression on retry. +CREATE TABLE t_node_try_again(a text); +INSERT INTO t_node_try_again SELECT chr(65 + g % 52) FROM generate_series(1, 5200) g; +ANALYZE t_node_try_again; +SELECT * FROM check_estimated_rows($$SELECT * FROM t_node_try_again WHERE a COLLATE "C" = 'A'$$); + estimated | actual +-----------+-------- + 100 | 100 +(1 row) + +DROP TABLE t_node_try_again; +-- Strips the topmost RelabelType from the extended statistics expression on retry. +CREATE TABLE t_statexpr_try_again (a text); +INSERT INTO t_statexpr_try_again SELECT chr(65 + g % 52) || g FROM generate_series(1, 5200) g; +CREATE STATISTICS t_statexpr_try_again_stats_1 ON (left(a, 1) COLLATE "C") FROM t_statexpr_try_again; +ANALYZE t_statexpr_try_again; +SELECT * FROM check_estimated_rows($$SELECT * FROM t_statexpr_try_again WHERE left(a, 1) = 'A'$$); + estimated | actual +-----------+-------- + 100 | 100 +(1 row) + +DROP STATISTICS t_statexpr_try_again_stats_1; +DROP TABLE t_statexpr_try_again; +CREATE TABLE phv_left(id int ); +CREATE TABLE phv_right(id int, name text COLLATE "C"); +INSERT INTO phv_left SELECT g FROM generate_series(1, 1100) AS g; +INSERT INTO phv_right SELECT g, + CASE WHEN g % 17 = 0 THEN NULL + ELSE chr(65 + (g % 26)) + END +FROM generate_series(1, 1000) AS g; +analyze phv_left, phv_right; +-- Strips the topmost RelabelType from the WHERE expression on retry. +\set in_pattern '''Seq Scan on phv_right''' +\set in_sql '''SELECT id,(COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) AS x FROM phv_right WHERE (COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) = ''''A'''' ''' +-- No matching statistics found. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 5 | 36.00 +(1 row) + +-- Matches the statistics for index phv_right_index_5 on retry. +CREATE INDEX phv_right_index_5 ON phv_right(COALESCE(name COLLATE "C", ''::text COLLATE "C")); +analyze phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 36 | 36.00 +(1 row) + +DROP INDEX phv_right_index_5; +-- Matches the statistics for extended statistics object phv_right_expr_stats_5 on retry. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 5 | 36.00 +(1 row) + +CREATE STATISTICS phv_right_expr_stats_5 ON (COALESCE(name COLLATE "C", ''::text COLLATE "C")) FROM phv_right; +ANALYZE phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 36 | 36.00 +(1 row) + +DROP STATISTICS phv_right_expr_stats_5; +-- Verify that stripping PlaceHolderVars and normalizing adjacent +-- RelabelTypes allows the planner to find statistics for index +-- expressions and extended statistics expressions. +\set in_sql '''SELECT l.id, r.id FROM phv_left AS l LEFT JOIN (SELECT id, (COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) AS x FROM phv_right) AS r ON r.id = l.id WHERE r.x COLLATE "C" = ''''A'''' ''' +-- No matching statistics found. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 5 | 36.00 +(1 row) + +-- Matches the statistics for index phv_right_index_5. +CREATE INDEX phv_right_index_5 ON phv_right(COALESCE(name COLLATE "C", ''::text COLLATE "C")); +analyze phv_right; +\set in_pattern_1 '''Bitmap Heap Scan on phv_right''' +\set in_pattern_2 '''Bitmap Index Scan on phv_right_index_5''' +SELECT :in_pattern_1 as SCAN,* FROM check_estimated_rows_by_pattern(:in_pattern_1, :in_sql) +UNION ALL +SELECT :in_pattern_2 as SCAN,* FROM check_estimated_rows_by_pattern(:in_pattern_2, :in_sql); + scan | estimated | actual +----------------------------------------+-----------+-------- + Bitmap Heap Scan on phv_right | 36 | 36.00 + Bitmap Index Scan on phv_right_index_5 | 36 | 36.00 +(2 rows) + +DROP INDEX phv_right_index_5; +-- Matches the statistics for extended statistics object phv_right_expr_stats_5. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 5 | 36.00 +(1 row) + +CREATE STATISTICS phv_right_expr_stats_5 ON (COALESCE(name COLLATE "C", ''::text COLLATE "C")) FROM phv_right; +ANALYZE phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + action | estimated | actual +-----------------------+-----------+-------- + Seq Scan on phv_right | 36 | 36.00 +(1 row) + +DROP STATISTICS phv_right_expr_stats_5; +DROP TABLE phv_left; +DROP TABLE phv_right; +DROP FUNCTION check_estimated_rows_by_pattern; +DROP FUNCTION check_estimated_rows; -- cleanup RESET search_path; SET client_min_messages TO warning; diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 87cea263..5fdfcbea 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -15,6 +15,25 @@ SET client_encoding TO UTF8; CREATE SCHEMA collate_tests; SET search_path = collate_tests; +create function check_estimated_rows(text) returns table (estimated int, actual int) +language plpgsql as +$$ +declare + ln text; + tmp text[]; + first_row bool := true; +begin + for ln in + execute format('explain analyze %s', $1) + loop + if first_row then + first_row := false; + tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)'); + return query select tmp[1]::int, tmp[2]::int; + end if; + end loop; +end; +$$; CREATE TABLE collate_test1 ( a int, @@ -565,6 +584,17 @@ SELECT * FROM test6a WHERE b = ARRAY['äbc'] COLLATE ctest_nondet; CREATE COLLATION case_sensitive (provider = icu, locale = ''); CREATE COLLATION case_insensitive (provider = icu, locale = '@colStrength=secondary', deterministic = false); +CREATE TABLE test_stats_ext_coll (a text, b text, c int); +INSERT INTO test_stats_ext_coll SELECT chr(65 + g % 52) FROM generate_series(1, 500) g; +CREATE STATISTICS test_stats_ext_coll_stx ON (a COLLATE case_insensitive) FROM test_stats_ext_coll; +ANALYZE test_stats_ext_coll; + +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE "C" $$); +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE "POSIX" $$); +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a COLLATE case_insensitive $$); +SELECT * FROM check_estimated_rows($$SELECT count(*) FROM test_stats_ext_coll GROUP BY a $$); +DROP TABLE test_stats_ext_coll; + SELECT 'abc' <= 'ABC' COLLATE case_sensitive, 'abc' >= 'ABC' COLLATE case_sensitive; SELECT 'abc' <= 'ABC' COLLATE case_insensitive, 'abc' >= 'ABC' COLLATE case_insensitive; @@ -1533,6 +1563,108 @@ SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = ' SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error DROP DOMAIN d1, d2; +-- Checks the estimated and actual row counts for the specified pattern +-- in the EXPLAIN ANALYZE output of the given SQL query. +CREATE FUNCTION check_estimated_rows_by_pattern(in_pattern text, in_sql text) RETURNS TABLE (estimated text, actual text) +LANGUAGE PLPGSQL AS +$$ +DECLARE + ln text; + tmp text[]; + tmp_pattern text; +BEGIN + FOR ln IN execute FORMAT('EXPLAIN ANALYZE %s', in_sql) + LOOP + tmp_pattern := FORMAT('(%s)', in_pattern); + tmp := REGEXP_MATCH(ln, tmp_pattern); + IF tmp IS NOT NULL THEN + tmp := REGEXP_MATCH(ln, 'rows=(\d*) .* rows=([0-9.]+)'); + RETURN QUERY SELECT tmp[1]::text, tmp[2]::text; + END IF; + END LOOP; +END; +$$; + +-- Verify that statistics can be found on retry when the initial +-- lookup fails due to a collation mismatch. + +-- Strips the topmost RelabelType from the WHERE expression on retry. +CREATE TABLE t_node_try_again(a text); +INSERT INTO t_node_try_again SELECT chr(65 + g % 52) FROM generate_series(1, 5200) g; +ANALYZE t_node_try_again; +SELECT * FROM check_estimated_rows($$SELECT * FROM t_node_try_again WHERE a COLLATE "C" = 'A'$$); +DROP TABLE t_node_try_again; + +-- Strips the topmost RelabelType from the extended statistics expression on retry. +CREATE TABLE t_statexpr_try_again (a text); +INSERT INTO t_statexpr_try_again SELECT chr(65 + g % 52) || g FROM generate_series(1, 5200) g; +CREATE STATISTICS t_statexpr_try_again_stats_1 ON (left(a, 1) COLLATE "C") FROM t_statexpr_try_again; +ANALYZE t_statexpr_try_again; +SELECT * FROM check_estimated_rows($$SELECT * FROM t_statexpr_try_again WHERE left(a, 1) = 'A'$$); +DROP STATISTICS t_statexpr_try_again_stats_1; +DROP TABLE t_statexpr_try_again; + +CREATE TABLE phv_left(id int ); +CREATE TABLE phv_right(id int, name text COLLATE "C"); + +INSERT INTO phv_left SELECT g FROM generate_series(1, 1100) AS g; +INSERT INTO phv_right SELECT g, + CASE WHEN g % 17 = 0 THEN NULL + ELSE chr(65 + (g % 26)) + END +FROM generate_series(1, 1000) AS g; +analyze phv_left, phv_right; + +-- Strips the topmost RelabelType from the WHERE expression on retry. +\set in_pattern '''Seq Scan on phv_right''' +\set in_sql '''SELECT id,(COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) AS x FROM phv_right WHERE (COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) = ''''A'''' ''' + +-- No matching statistics found. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + +-- Matches the statistics for index phv_right_index_5 on retry. +CREATE INDEX phv_right_index_5 ON phv_right(COALESCE(name COLLATE "C", ''::text COLLATE "C")); +analyze phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); +DROP INDEX phv_right_index_5; + +-- Matches the statistics for extended statistics object phv_right_expr_stats_5 on retry. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); +CREATE STATISTICS phv_right_expr_stats_5 ON (COALESCE(name COLLATE "C", ''::text COLLATE "C")) FROM phv_right; +ANALYZE phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); +DROP STATISTICS phv_right_expr_stats_5; + +-- Verify that stripping PlaceHolderVars and normalizing adjacent +-- RelabelTypes allows the planner to find statistics for index +-- expressions and extended statistics expressions. +\set in_sql '''SELECT l.id, r.id FROM phv_left AS l LEFT JOIN (SELECT id, (COALESCE(name COLLATE "C", ''''''''::text COLLATE "C") COLLATE case_insensitive) AS x FROM phv_right) AS r ON r.id = l.id WHERE r.x COLLATE "C" = ''''A'''' ''' + +-- No matching statistics found. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); + +-- Matches the statistics for index phv_right_index_5. +CREATE INDEX phv_right_index_5 ON phv_right(COALESCE(name COLLATE "C", ''::text COLLATE "C")); +analyze phv_right; +\set in_pattern_1 '''Bitmap Heap Scan on phv_right''' +\set in_pattern_2 '''Bitmap Index Scan on phv_right_index_5''' +SELECT :in_pattern_1 as SCAN,* FROM check_estimated_rows_by_pattern(:in_pattern_1, :in_sql) +UNION ALL +SELECT :in_pattern_2 as SCAN,* FROM check_estimated_rows_by_pattern(:in_pattern_2, :in_sql); +DROP INDEX phv_right_index_5; + +-- Matches the statistics for extended statistics object phv_right_expr_stats_5. +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); +CREATE STATISTICS phv_right_expr_stats_5 ON (COALESCE(name COLLATE "C", ''::text COLLATE "C")) FROM phv_right; +ANALYZE phv_right; +SELECT :in_pattern as action,* FROM check_estimated_rows_by_pattern(:in_pattern, :in_sql); +DROP STATISTICS phv_right_expr_stats_5; + +DROP TABLE phv_left; +DROP TABLE phv_right; +DROP FUNCTION check_estimated_rows_by_pattern; +DROP FUNCTION check_estimated_rows; + -- cleanup RESET search_path; SET client_min_messages TO warning; -- 2.43.0