From ad81aa161e49b5278fe4e8d6b58de0f05cebcd0f Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Mon, 29 Sep 2025 15:20:10 -0400 Subject: [PATCH] start of flag-based reporting currently this only works for the join path hook, not the join rel or scan hook the results are emitted as warnings instead of being properly structured i have a feeling that there needs to be more interaction between the join path stuff and the join rel stuff - e.g. PARTITIONWISE((a b)) conflicts with HASH_JOIN(b) but I don't think the current code can detect that we also need to split off the stuff that gets handled in the join rel hook from a trove POV, to avoid making duplicate entries that will confuse this reporting an incidental note is that i think we should teach the advice parser to ignore comments, so that we can emit something that looks like a comment as a report, e.g. JOIN_ORDER(a b) /* matched, conflicted */ JOIN_ORDER(a b) /* partial match only */ JOIN_ORDER(a b) /* not matched */ INDEX_SCAN(this that_idx) /* matched, inapplicable */ --- contrib/pg_plan_advice/pgpa_planner.c | 161 +++++++++++++++++++++----- contrib/pg_plan_advice/pgpa_trove.c | 35 +++++- contrib/pg_plan_advice/pgpa_trove.h | 32 +++++ 3 files changed, 197 insertions(+), 31 deletions(-) diff --git a/contrib/pg_plan_advice/pgpa_planner.c b/contrib/pg_plan_advice/pgpa_planner.c index 2096332b6f..b71db3b02a 100644 --- a/contrib/pg_plan_advice/pgpa_planner.c +++ b/contrib/pg_plan_advice/pgpa_planner.c @@ -156,17 +156,20 @@ static uint64 pgpa_join_strategy_mask_from_advice_tag(pgpa_advice_tag_type tag); static bool pgpa_join_order_permits_join(int outer_count, int inner_count, pgpa_trove_key *tkeys, char *plan_name, - pgpa_advice_target *target); + pgpa_trove_entry *entry); static bool pgpa_join_method_permits_join(int outer_count, int inner_count, pgpa_trove_key *tkeys, char *plan_name, - pgpa_advice_target *target, + pgpa_trove_entry *entry, bool *restrict_method); +static void pgpa_set_entry_flags(pgpa_trove_entry *entries, Bitmapset *indexes, + int flags); static inline void pgpa_ri_checker_save(pgpa_planner_state *pps, PlannerInfo *root, RelOptInfo *rel); -static void pgpa_ri_checker_validate(pgpa_planner_state *pps, PlannedStmt *pstmt); +static void pgpa_ri_checker_validate(pgpa_planner_state *pps, + PlannedStmt *pstmt); /* * Install planner-related hooks. @@ -515,6 +518,28 @@ pgpa_planner_setup(PlannerGlobal *glob, Query *parse, const char *query_string, } } +/* + * XXX obviously not the right thing + * + * XXX note that any overlap between troves is a real problem here -- we need + * one trove entry per tag/target combo + */ +static void +pgpa_planner_stupid_warning(pgpa_trove *trove, pgpa_trove_lookup_type type) +{ + pgpa_trove_entry *entries; + int nentries; + + pgpa_trove_lookup_all(trove, type, &entries, &nentries); + for (int i = 0; i < nentries; ++i) + { + pgpa_trove_entry *entry = &entries[i]; + + elog(WARNING, "%s -> %d", pgpa_cstring_trove_entry(entry), + entry->flags); + } +} + /* * Carry out whatever work we want to do after planning is complete. */ @@ -527,6 +552,12 @@ pgpa_planner_shutdown(PlannerGlobal *glob, Query *parse, /* Fetch our private state, set up by pgpa_planner_setup(). */ pps = GetPlannerGlobalExtensionState(glob, planner_extension_id); + if (pps != NULL && pps->trove != NULL) + { + pgpa_planner_stupid_warning(pps->trove, PGPA_TROVE_LOOKUP_SCAN); + pgpa_planner_stupid_warning(pps->trove, PGPA_TROVE_LOOKUP_JOIN); + } + /* * If assertions are enabled, cross-check the generated range table * identifiers. @@ -723,26 +754,29 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p, pgpa_join_state *pjs) { int i = -1; - bool join_ok = true; + Bitmapset *jo_permit_indexes = NULL; + Bitmapset *jo_deny_indexes = NULL; + Bitmapset *jm_indexes = NULL; + bool jm_conflict = false; uint32 pgs_mask = 0; /* Iterate over all possibly-relevant advice. */ while ((i = bms_next_member(pjs->indexes, i)) >= 0) { pgpa_trove_entry *entry = &pjs->entries[i]; - uint32 my_pgs_mask = 0; + uint32 my_pgs_mask; /* Handle join order advice. */ if (entry->tag == PGPA_TAG_JOIN_ORDER) { - if (!join_ok) - continue; - if (!pgpa_join_order_permits_join(pjs->outer_count, - pjs->inner_count, - pjs->tkeys, - plan_name, - entry->target)) - join_ok = false; + if (pgpa_join_order_permits_join(pjs->outer_count, + pjs->inner_count, + pjs->tkeys, + plan_name, + entry)) + jo_permit_indexes = bms_add_member(jo_permit_indexes, i); + else + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); continue; } @@ -756,15 +790,15 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p, pjs->inner_count, pjs->tkeys, plan_name, - entry->target, + entry, &restrict_method)) - join_ok = false; + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); else if (restrict_method) { - /* XXX wrong way to report problems */ + jo_permit_indexes = bms_add_member(jo_permit_indexes, i); + jm_indexes = bms_add_member(jo_permit_indexes, i); if (pgs_mask != 0 && pgs_mask != my_pgs_mask) - elog(WARNING, - "conflicting masks: %u vs. %u", pgs_mask, my_pgs_mask); + jm_conflict = true; pgs_mask = my_pgs_mask; } continue; @@ -803,28 +837,62 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p, pjs->inner_count, pjs->tkeys, plan_name, - entry->target, + entry, &restrict_method)) - join_ok = false; + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); else if (restrict_method) { + jo_permit_indexes = bms_add_member(jo_permit_indexes, i); if (!jt_unique && !jt_non_unique) { - /* XXX wrong way to report problems */ - elog(WARNING, "%s not a semijoin jointype=%d", - pgpa_cstring_trove_entry(entry), jointype); + /* + * This doesn't seem to be a semijoin to which SJ_UNIQUE + * or SJ_NON_UNIQUE can be applied. + */ + entry->flags |= PGPA_TE_INAPPLICABLE; } else if (advice_unique != jt_unique) - join_ok = false; + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); } continue; } } - /* Apply computed pgs_mask. */ - if (!join_ok) + /* + * If the advice indicates both that this join order is permissible and + * also that it isn't, then mark advice related to the join order as + * conflicting. + */ + if (jo_permit_indexes != NULL && jo_deny_indexes != NULL) + { + pgpa_set_entry_flags(pjs->entries, jo_permit_indexes, + PGPA_TE_CONFLICTING); + pgpa_set_entry_flags(pjs->entries, jo_deny_indexes, + PGPA_TE_CONFLICTING); + } + + /* + * If more than one join method specification is relevant here and they + * differ, mark them all as conflicting. + */ + if (jm_conflict) + pgpa_set_entry_flags(pjs->entries, jm_indexes, + PGPA_TE_CONFLICTING); + + /* + * If we were advised to deny this join order, then do so. However, if we + * were also advised to permit it, then do nothing, since the advice + * conflicts. + */ + if (jo_deny_indexes != NULL && jo_permit_indexes == NULL) *pgs_mask_p = 0; - else if (pgs_mask != 0) + + /* + * If we were advised to restrict the join method, then do so. However, + * if we got conflicting join method advice or were also advised to reject + * this join order completely, then instead do nothing. + */ + if (pgs_mask != 0 && !jm_conflict && jo_deny_indexes == NULL) *pgs_mask_p = pgs_mask; } @@ -864,14 +932,18 @@ static bool pgpa_join_order_permits_join(int outer_count, int inner_count, pgpa_trove_key *tkeys, char *plan_name, - pgpa_advice_target *target) + pgpa_trove_entry *entry) { bool loop = true; bool sublist = false; int length; int outer_length; + pgpa_advice_target *target = entry->target; pgpa_advice_target *prefix_target; + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_TE_MATCH_PARTIAL; + /* * Find the innermost sublist that contains all keys; if no sublist does, * then continue processing with the toplevel list. @@ -971,6 +1043,16 @@ pgpa_join_order_permits_join(int outer_count, int inner_count, tkm = pgpa_trove_keys_match_target(inner_count, tkeys + outer_count, plan_name, inner_target); + + /* + * Before returning, consider whether we need to mark this entry as + * fully matched. If we found every item but one on the lefthand side + * of the join and the last item on the righthand side of the join, + * then the answer is yes. + */ + if (outer_length + 1 == length && tkm == PGPA_TKM_EQUAL) + entry->flags |= PGPA_TE_MATCH_FULL; + return (tkm == PGPA_TKM_EQUAL); } @@ -1016,13 +1098,17 @@ static bool pgpa_join_method_permits_join(int outer_count, int inner_count, pgpa_trove_key *tkeys, char *plan_name, - pgpa_advice_target *target, + pgpa_trove_entry *entry, bool *restrict_method) { + pgpa_advice_target *target = entry->target; pgpa_tkm_type inner_tkm; pgpa_tkm_type outer_tkm; pgpa_tkm_type join_tkm; + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_TE_MATCH_PARTIAL; + *restrict_method = false; /* @@ -1039,6 +1125,7 @@ pgpa_join_method_permits_join(int outer_count, int inner_count, target); if (inner_tkm == PGPA_TKM_EQUAL) { + entry->flags |= PGPA_TE_MATCH_FULL; *restrict_method = true; return true; } @@ -1320,6 +1407,22 @@ pgpa_ri_checker_hash_key(pgpa_ri_checker_key key) #endif +/* + * Set PGPA_TE_* flags on a set of trove entries. + */ +static void +pgpa_set_entry_flags(pgpa_trove_entry *entries, Bitmapset *indexes, int flags) +{ + int i = -1; + + while ((i = bms_next_member(indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &entries[i]; + + entry->flags |= flags; + } +} + /* * Save the range table identifier for one relation for future cross-checking. */ diff --git a/contrib/pg_plan_advice/pgpa_trove.c b/contrib/pg_plan_advice/pgpa_trove.c index f177adc170..da2a1a1b1c 100644 --- a/contrib/pg_plan_advice/pgpa_trove.c +++ b/contrib/pg_plan_advice/pgpa_trove.c @@ -288,6 +288,27 @@ pgpa_trove_lookup(pgpa_trove *trove, pgpa_trove_lookup_type type, result->indexes = indexes; } +/* + * Return all entries in a trove slice to the caller. + * + * The first two arguments are input arguments, and the remainder are output + * arguments. + */ +void +pgpa_trove_lookup_all(pgpa_trove *trove, pgpa_trove_lookup_type type, + pgpa_trove_entry **entries, int *nentries) +{ + pgpa_trove_slice *tslice; + + if (type == PGPA_TROVE_LOOKUP_SCAN) + tslice = &trove->scan; + else + tslice = &trove->join; + + *entries = tslice->entries; + *nentries = tslice->nused; +} + /* * Match trove keys to advice targets and return an enum value indicating * the relationship between the set of keys and the set of targets. @@ -342,9 +363,18 @@ pgpa_cstring_trove_entry(pgpa_trove_entry *entry) StringInfoData buf; initStringInfo(&buf); - appendStringInfo(&buf, "%s(", pgpa_cstring_advice_tag(entry->tag)); + appendStringInfo(&buf, "%s", pgpa_cstring_advice_tag(entry->tag)); + + /* JOIN_ORDER tags are transformed by pgpa_build_trove; undo that here */ + if (entry->tag != PGPA_TAG_JOIN_ORDER) + appendStringInfoChar(&buf, '('); + else + Assert(entry->target->ttype == PGPA_TARGET_ORDERED_LIST); + pgpa_format_advice_target(&buf, entry->target); - appendStringInfoChar(&buf, ')'); + + if (entry->tag != PGPA_TAG_JOIN_ORDER) + appendStringInfoChar(&buf, ')'); return buf.data; } @@ -372,6 +402,7 @@ pgpa_trove_add_to_slice(pgpa_trove_slice *tslice, entry = &tslice->entries[tslice->nused]; entry->tag = tag; entry->target = target; + entry->flags = 0; pgpa_trove_add_to_hash(tslice->hash, target, tslice->nused); diff --git a/contrib/pg_plan_advice/pgpa_trove.h b/contrib/pg_plan_advice/pgpa_trove.h index 0324f18e96..f85cb23973 100644 --- a/contrib/pg_plan_advice/pgpa_trove.h +++ b/contrib/pg_plan_advice/pgpa_trove.h @@ -19,6 +19,33 @@ typedef struct pgpa_trove pgpa_trove; +/* + * Flags that can be set on a pgpa_trove_entry to indicate what happened when + * trying to plan using advice. + * + * PGPA_TE_MATCH_PARTIAL means that we found some part of the query that at + * least partially matched the target; e.g. given JOIN_ORDER(a b), this would + * be set if we ever saw any joinrel including either "a" or "b". + * + * PGPA_TE_MATCH_FULL means that we found an exact match for the target; e.g. + * given JOIN_ORDER(a b), this would be set if we saw a joinrel containing + * exactly "a" and "b" and nothing else. + * + * PGPA_TE_INAPPLICABLE means that the advice doesn't properly apply to the + * target; e.g. INDEX_SCAN(foo bar_idx) would be so marked if bar_idx does not + * exist on foo. The fact that this bit has been set does not mean that the + * advice had no effect. + * + * PGPA_TE_CONFLICTING means that a conflict was detected between what this + * advice wants and what some other plan advice wants; e.g. JOIN_ORDER(a b) + * would conflict with HASH_JOIN(a), because the former requires "a" to be the + * outer table while the latter requires it to be the inner table. + */ +#define PGPA_TE_MATCH_PARTIAL 0x0001 +#define PGPA_TE_MATCH_FULL 0x0002 +#define PGPA_TE_INAPPLICABLE 0x0004 +#define PGPA_TE_CONFLICTING 0x0008 + /* * Each entry in a trove of advice represents the application of a tag to * a single target. @@ -27,6 +54,7 @@ typedef struct pgpa_trove_entry { pgpa_advice_tag_type tag; pgpa_advice_target *target; + int flags; } pgpa_trove_entry; /* @@ -108,6 +136,10 @@ extern void pgpa_trove_lookup(pgpa_trove *trove, pgpa_trove_key *tkeys, char *plan_name, pgpa_trove_result *result); +extern void pgpa_trove_lookup_all(pgpa_trove *trove, + pgpa_trove_lookup_type type, + pgpa_trove_entry **entries, + int *nentries); extern pgpa_tkm_type pgpa_trove_keys_match_target(int nkeys, pgpa_trove_key *tkeys, char *plan_name, -- 2.39.5