From ce373a44fe5202e24734c4122f0179362ff5a0a7 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 15 Apr 2026 08:01:41 +0200 Subject: [PATCH v0] Skip merge join paths when opfamily lacks ordering operators An incomplete btree opfamily definition could cause the planner to construct a merge join path that cannot be implemented. --- src/backend/optimizer/path/equivclass.c | 39 ++++++++++++++++++++++++ src/backend/optimizer/path/joinpath.c | 23 ++++++++++++++ src/backend/optimizer/path/pathkeys.c | 27 ++++++++++++++++ src/include/optimizer/paths.h | 3 ++ src/test/regress/expected/equivclass.out | 36 ++++++++++++++++++++++ src/test/regress/sql/equivclass.sql | 26 ++++++++++++++++ 6 files changed, 154 insertions(+) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index e3697df51a2..67904bb5f47 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -1137,6 +1137,45 @@ relation_can_be_sorted_early(PlannerInfo *root, RelOptInfo *rel, return true; } +/* + * ec_has_sortable_member + * Check whether an EquivalenceClass has at least one non-constant member + * belonging to 'relids' for which a sort operator of the given compare + * type exists in the specified opfamily. Returns false if no qualifying + * member has a valid sort operator, which can happen with incomplete + * opfamily definitions. + */ +bool +ec_has_sortable_member(EquivalenceClass *ec, Relids relids, + Oid opfamily, CompareType cmptype) +{ + EquivalenceMemberIterator it; + EquivalenceMember *em; + + setup_eclass_member_iterator(&it, ec, relids); + while ((em = eclass_member_iterator_next(&it)) != NULL) + { + if (em->em_is_const) + continue; + + /* + * Only consider members that belong to the specified relation. + * The iterator returns all non-child members regardless of relids, + * so we must filter explicitly. + */ + if (!bms_is_subset(em->em_relids, relids)) + continue; + + if (OidIsValid(get_opfamily_member_for_cmptype(opfamily, + em->em_datatype, + em->em_datatype, + cmptype))) + return true; + } + + return false; +} + /* * generate_base_implied_equalities * Generate any restriction clauses that we can deduce from equivalence diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c index 713283a73aa..210d49a4629 100644 --- a/src/backend/optimizer/path/joinpath.c +++ b/src/backend/optimizer/path/joinpath.c @@ -1535,6 +1535,15 @@ sort_inner_and_outer(PlannerInfo *root, cur_mergeclauses, outerkeys); + /* + * Verify that both sides can actually be sorted on these pathkeys. + * An incomplete opfamily (e.g. equality without ordering operators) + * could pass the mergejoinable check but fail at sort time. + */ + if (!pathkeys_are_sortable_for_rel(outerkeys, outerrel->relids) || + !pathkeys_are_sortable_for_rel(innerkeys, innerrel->relids)) + continue; + /* Build pathkeys representing output sort order */ merge_pathkeys = build_join_pathkeys(root, joinrel, jointype, outerkeys); @@ -1641,6 +1650,20 @@ generate_mergejoin_paths(PlannerInfo *root, mergeclauses, outerpath->pathkeys); + /* + * Verify that the inner side can be sorted on these pathkeys. An + * incomplete opfamily (e.g. equality without ordering operators) could + * allow a mergejoin path that cannot be implemented. + * + * We only check the inner side here because the outer path is already + * ordered (this function is called from match_unsorted_outer where the + * outer path's existing pathkeys drive the merge). The outer side + * never needs an explicit sort, so outersortkeys is always NIL in the + * try_mergejoin_path call below. + */ + if (!pathkeys_are_sortable_for_rel(innersortkeys, innerrel->relids)) + return; + /* * Generate a mergejoin on the basis of sorting the cheapest inner. Since * a sort will be needed, only cheapest total cost matters. (But diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 5eb71635d15..83e5e42d393 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -353,6 +353,33 @@ pathkeys_contained_in(List *keys1, List *keys2) return false; } +/* + * pathkeys_are_sortable_for_rel + * Check whether every pathkey in the list has a usable sort operator + * for at least one EquivalenceClass member belonging to 'relids'. + * + * Returns false if any pathkey's opfamily lacks the required ordering + * operator for all EC members of the given relation. This catches + * incomplete opfamily definitions (e.g. equality registered without + * corresponding ordering operators) that would cause an error later + * in prepare_sort_from_pathkeys(). + */ +bool +pathkeys_are_sortable_for_rel(List *pathkeys, Relids relids) +{ + ListCell *lc; + + foreach(lc, pathkeys) + { + PathKey *pk = (PathKey *) lfirst(lc); + + if (!ec_has_sortable_member(pk->pk_eclass, relids, + pk->pk_opfamily, pk->pk_cmptype)) + return false; + } + return true; +} + /* * group_keys_reorder_by_pathkeys * Reorder GROUP BY pathkeys and clauses to match the input pathkeys. diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 17f2099ec3b..6b9adca2921 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -157,6 +157,8 @@ extern EquivalenceMember *find_computable_ec_member(PlannerInfo *root, extern bool relation_can_be_sorted_early(PlannerInfo *root, RelOptInfo *rel, EquivalenceClass *ec, bool require_parallel_safe); +extern bool ec_has_sortable_member(EquivalenceClass *ec, Relids relids, + Oid opfamily, CompareType cmptype); extern void generate_base_implied_equalities(PlannerInfo *root); extern List *generate_join_implied_equalities(PlannerInfo *root, Relids join_relids, @@ -225,6 +227,7 @@ typedef enum extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2); extern bool pathkeys_contained_in(List *keys1, List *keys2); extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common); +extern bool pathkeys_are_sortable_for_rel(List *pathkeys, Relids relids); extern List *get_useful_group_keys_orderings(PlannerInfo *root, Path *path); extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys, Relids required_outer, diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out index ad8ab294ff6..7c7ac218b0d 100644 --- a/src/test/regress/expected/equivclass.out +++ b/src/test/regress/expected/equivclass.out @@ -485,6 +485,42 @@ explain (costs off) -- this should not require a sort Filter: (f1 = 'foo'::name) (2 rows) +-- +-- check that incomplete opfamily does not cause merge join to crash. +-- the < operator for int8alias1 is temporarily removed from the opfamily +-- to simulate an incomplete definition (equality without ordering operators). +-- +begin; +alter operator family integer_ops using btree drop operator 1 (int8alias1, int8alias1); +set enable_nestloop = off; +set enable_hashjoin = off; +set enable_mergejoin = on; +-- should fall back to non-merge plan without error +explain (costs off) + select * from ec1, + (select ff + 1 as x from + (select ff + 2 as ff from ec1 + union all + select ff + 3 as ff from ec1) ss0 + union all + select ff + 4 as x from ec1) as ss1 + where ss1.x = ec1.f1 and ec1.ff = 42::int8; + QUERY PLAN +----------------------------------------------------- + Nested Loop + Disabled: true + -> Index Scan using ec1_pkey on ec1 + Index Cond: (ff = '42'::bigint) + -> Append + -> Index Scan using ec1_expr2 on ec1 ec1_1 + Index Cond: (((ff + 2) + 1) = ec1.f1) + -> Seq Scan on ec1 ec1_2 + Filter: (((ff + 3) + 1) = ec1.f1) + -> Index Scan using ec1_expr4 on ec1 ec1_3 + Index Cond: ((ff + 4) = ec1.f1) +(11 rows) + +abort; -- -- test handling of merge/hash clauses that do not have valid commutators -- diff --git a/src/test/regress/sql/equivclass.sql b/src/test/regress/sql/equivclass.sql index 7fc2159349b..b06c23c0520 100644 --- a/src/test/regress/sql/equivclass.sql +++ b/src/test/regress/sql/equivclass.sql @@ -286,6 +286,32 @@ create temp view overview as explain (costs off) -- this should not require a sort select * from overview where sqli = 'foo' order by sqli; +-- +-- check that incomplete opfamily does not cause merge join to crash. +-- the < operator for int8alias1 is temporarily removed from the opfamily +-- to simulate an incomplete definition (equality without ordering operators). +-- +begin; + +alter operator family integer_ops using btree drop operator 1 (int8alias1, int8alias1); + +set enable_nestloop = off; +set enable_hashjoin = off; +set enable_mergejoin = on; + +-- should fall back to non-merge plan without error +explain (costs off) + select * from ec1, + (select ff + 1 as x from + (select ff + 2 as ff from ec1 + union all + select ff + 3 as ff from ec1) ss0 + union all + select ff + 4 as x from ec1) as ss1 + where ss1.x = ec1.f1 and ec1.ff = 42::int8; + +abort; + -- -- test handling of merge/hash clauses that do not have valid commutators -- -- 2.53.0