Skip to content

Commit dba1a32

Browse files
committed
Support AQUMV exact-match for multi-table JOIN queries
Add a new AQUMV code path that rewrites multi-table JOIN queries to scan materialized views when the query exactly matches the MV definition. This compares the saved raw parse tree against stored viewQuery from gp_matview_aux, bypassing the single-table AQUMV logic entirely. This enables significant query acceleration for common analytical patterns: instead of repeatedly computing expensive multi-table joins at query time, the planner can directly read pre-computed results from the materialized view, turning O(N*M) join operations into a simple sequential scan. For example, given: CREATE MATERIALIZED VIEW mv AS SELECT t1.a, t2.b FROM t1 JOIN t2 ON t1.a = t2.a; -- Before (GUC off): original join plan Gather Motion 3:1 -> Hash Join Hash Cond: (t1.a = t2.a) -> Seq Scan on t1 -> Hash -> Seq Scan on t2 -- After (GUC on): rewritten to MV scan Gather Motion 3:1 -> Seq Scan on mv
1 parent 406f088 commit dba1a32

File tree

6 files changed

+2206
-36
lines changed

6 files changed

+2206
-36
lines changed

src/backend/optimizer/plan/aqumv.c

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,3 +996,348 @@ groupby_query_rewrite(PlannerInfo *subroot,
996996
subroot->append_rel_list = NIL;
997997
return true;
998998
}
999+
1000+
/*
1001+
* aqumv_query_is_exact_match
1002+
*
1003+
* Compare two Query trees for semantic identity. Both should be at the
1004+
* same preprocessing stage (raw parser output). Returns true only if
1005+
* they are structurally identical in all query-semantics fields.
1006+
*/
1007+
static bool
1008+
aqumv_query_is_exact_match(Query *raw_parse, Query *viewQuery)
1009+
{
1010+
/* Both must be CMD_SELECT */
1011+
if (raw_parse->commandType != CMD_SELECT ||
1012+
viewQuery->commandType != CMD_SELECT)
1013+
return false;
1014+
1015+
/* Same number of range table entries */
1016+
if (list_length(raw_parse->rtable) != list_length(viewQuery->rtable))
1017+
return false;
1018+
1019+
/* Compare range tables (table OIDs, join types, aliases structure) */
1020+
if (!equal(raw_parse->rtable, viewQuery->rtable))
1021+
return false;
1022+
1023+
/* Compare join tree (FROM clause + WHERE quals) */
1024+
if (!equal(raw_parse->jointree, viewQuery->jointree))
1025+
return false;
1026+
1027+
/* Compare target list entries: expressions and sort/group refs */
1028+
if (list_length(raw_parse->targetList) != list_length(viewQuery->targetList))
1029+
return false;
1030+
{
1031+
ListCell *lc1, *lc2;
1032+
forboth(lc1, raw_parse->targetList, lc2, viewQuery->targetList)
1033+
{
1034+
TargetEntry *tle1 = lfirst_node(TargetEntry, lc1);
1035+
TargetEntry *tle2 = lfirst_node(TargetEntry, lc2);
1036+
if (!equal(tle1->expr, tle2->expr))
1037+
return false;
1038+
if (tle1->resjunk != tle2->resjunk)
1039+
return false;
1040+
if (tle1->ressortgroupref != tle2->ressortgroupref)
1041+
return false;
1042+
}
1043+
}
1044+
1045+
/* Compare GROUP BY, HAVING, ORDER BY, DISTINCT, LIMIT */
1046+
if (!equal(raw_parse->groupClause, viewQuery->groupClause))
1047+
return false;
1048+
if (!equal(raw_parse->havingQual, viewQuery->havingQual))
1049+
return false;
1050+
if (!equal(raw_parse->sortClause, viewQuery->sortClause))
1051+
return false;
1052+
if (!equal(raw_parse->distinctClause, viewQuery->distinctClause))
1053+
return false;
1054+
if (!equal(raw_parse->limitCount, viewQuery->limitCount))
1055+
return false;
1056+
if (!equal(raw_parse->limitOffset, viewQuery->limitOffset))
1057+
return false;
1058+
1059+
/* Compare boolean flags */
1060+
if (raw_parse->hasAggs != viewQuery->hasAggs)
1061+
return false;
1062+
if (raw_parse->hasWindowFuncs != viewQuery->hasWindowFuncs)
1063+
return false;
1064+
if (raw_parse->hasDistinctOn != viewQuery->hasDistinctOn)
1065+
return false;
1066+
1067+
return true;
1068+
}
1069+
1070+
/*
1071+
* answer_query_using_materialized_views_for_join
1072+
*
1073+
* Handle multi-table JOIN queries via exact-match comparison.
1074+
* This is completely independent from the single-table AQUMV code path.
1075+
*
1076+
* We compare the saved raw parse tree (before any planner preprocessing)
1077+
* against the stored viewQuery from gp_matview_aux. On exact match,
1078+
* rewrite the query to a simple SELECT FROM mv.
1079+
*/
1080+
RelOptInfo *
1081+
answer_query_using_materialized_views_for_join(PlannerInfo *root, AqumvContext aqumv_context)
1082+
{
1083+
RelOptInfo *current_rel = aqumv_context->current_rel;
1084+
query_pathkeys_callback qp_callback = aqumv_context->qp_callback;
1085+
Query *parse = root->parse;
1086+
Query *raw_parse = root->aqumv_raw_parse;
1087+
RelOptInfo *mv_final_rel = current_rel;
1088+
Relation matviewRel;
1089+
Relation mvauxDesc;
1090+
TupleDesc mvaux_tupdesc;
1091+
SysScanDesc mvscan;
1092+
HeapTuple tup;
1093+
Form_gp_matview_aux mvaux_tup;
1094+
bool need_close = false;
1095+
1096+
/* Must have the saved raw parse tree. */
1097+
if (raw_parse == NULL)
1098+
return mv_final_rel;
1099+
1100+
/* Must be a join query (more than one table in FROM). */
1101+
if (list_length(raw_parse->rtable) <= 1)
1102+
return mv_final_rel;
1103+
1104+
/* Basic eligibility checks (same as single-table AQUMV). */
1105+
if (parse->commandType != CMD_SELECT ||
1106+
parse->rowMarks != NIL ||
1107+
parse->scatterClause != NIL ||
1108+
parse->cteList != NIL ||
1109+
parse->setOperations != NULL ||
1110+
parse->hasModifyingCTE ||
1111+
parse->parentStmtType == PARENTSTMTTYPE_REFRESH_MATVIEW ||
1112+
parse->parentStmtType == PARENTSTMTTYPE_CTAS ||
1113+
contain_mutable_functions((Node *) raw_parse) ||
1114+
parse->hasSubLinks)
1115+
return mv_final_rel;
1116+
1117+
mvauxDesc = table_open(GpMatviewAuxId, AccessShareLock);
1118+
mvaux_tupdesc = RelationGetDescr(mvauxDesc);
1119+
1120+
mvscan = systable_beginscan(mvauxDesc, InvalidOid, false,
1121+
NULL, 0, NULL);
1122+
1123+
while (HeapTupleIsValid(tup = systable_getnext(mvscan)))
1124+
{
1125+
Datum view_query_datum;
1126+
char *view_query_str;
1127+
bool is_null;
1128+
Query *viewQuery;
1129+
RangeTblEntry *mvrte;
1130+
PlannerInfo *subroot;
1131+
TupleDesc mv_tupdesc;
1132+
1133+
CHECK_FOR_INTERRUPTS();
1134+
if (need_close)
1135+
table_close(matviewRel, AccessShareLock);
1136+
1137+
mvaux_tup = (Form_gp_matview_aux) GETSTRUCT(tup);
1138+
matviewRel = table_open(mvaux_tup->mvoid, AccessShareLock);
1139+
need_close = true;
1140+
1141+
if (!RelationIsPopulated(matviewRel))
1142+
continue;
1143+
1144+
/* MV must be up-to-date (IVM is always current). */
1145+
if (!RelationIsIVM(matviewRel) &&
1146+
!MatviewIsGeneralyUpToDate(RelationGetRelid(matviewRel)))
1147+
continue;
1148+
1149+
/* Get a copy of view query. */
1150+
view_query_datum = heap_getattr(tup,
1151+
Anum_gp_matview_aux_view_query,
1152+
mvaux_tupdesc,
1153+
&is_null);
1154+
1155+
view_query_str = TextDatumGetCString(view_query_datum);
1156+
viewQuery = copyObject(stringToNode(view_query_str));
1157+
pfree(view_query_str);
1158+
Assert(IsA(viewQuery, Query));
1159+
1160+
/* Skip single-table viewQueries (handled by existing AQUMV). */
1161+
if (list_length(viewQuery->rtable) <= 1)
1162+
continue;
1163+
1164+
/* Exact match comparison between raw parse and view query. */
1165+
if (!aqumv_query_is_exact_match(raw_parse, viewQuery))
1166+
continue;
1167+
1168+
/*
1169+
* We have an exact match. Rewrite viewQuery to:
1170+
* SELECT mv.col1, mv.col2, ... FROM mv
1171+
*/
1172+
mv_tupdesc = RelationGetDescr(matviewRel);
1173+
1174+
/* Build new target list referencing MV columns. */
1175+
{
1176+
List *new_tlist = NIL;
1177+
ListCell *lc;
1178+
int attnum = 0;
1179+
1180+
foreach(lc, viewQuery->targetList)
1181+
{
1182+
TargetEntry *old_tle = lfirst_node(TargetEntry, lc);
1183+
TargetEntry *new_tle;
1184+
Var *newVar;
1185+
Form_pg_attribute attr;
1186+
1187+
if (old_tle->resjunk)
1188+
continue;
1189+
1190+
attnum++;
1191+
attr = TupleDescAttr(mv_tupdesc, attnum - 1);
1192+
1193+
newVar = makeVar(1,
1194+
attr->attnum,
1195+
attr->atttypid,
1196+
attr->atttypmod,
1197+
attr->attcollation,
1198+
0);
1199+
newVar->location = -1;
1200+
1201+
new_tle = makeTargetEntry((Expr *) newVar,
1202+
(AttrNumber) attnum,
1203+
old_tle->resname,
1204+
false);
1205+
new_tlist = lappend(new_tlist, new_tle);
1206+
}
1207+
1208+
viewQuery->targetList = new_tlist;
1209+
}
1210+
1211+
/* Create new RTE for the MV. */
1212+
mvrte = makeNode(RangeTblEntry);
1213+
mvrte->rtekind = RTE_RELATION;
1214+
mvrte->relid = RelationGetRelid(matviewRel);
1215+
mvrte->relkind = RELKIND_MATVIEW;
1216+
mvrte->rellockmode = AccessShareLock;
1217+
mvrte->inh = false;
1218+
mvrte->inFromCl = true;
1219+
1220+
/* Build eref with column names from the MV's TupleDesc. */
1221+
{
1222+
Alias *eref = makeAlias(RelationGetRelationName(matviewRel), NIL);
1223+
int i;
1224+
for (i = 0; i < mv_tupdesc->natts; i++)
1225+
{
1226+
Form_pg_attribute attr = TupleDescAttr(mv_tupdesc, i);
1227+
if (!attr->attisdropped)
1228+
eref->colnames = lappend(eref->colnames,
1229+
makeString(pstrdup(NameStr(attr->attname))));
1230+
else
1231+
eref->colnames = lappend(eref->colnames,
1232+
makeString(pstrdup("")));
1233+
}
1234+
mvrte->eref = eref;
1235+
mvrte->alias = makeAlias(RelationGetRelationName(matviewRel), NIL);
1236+
}
1237+
1238+
viewQuery->rtable = list_make1(mvrte);
1239+
viewQuery->jointree = makeFromExpr(list_make1(makeNode(RangeTblRef)), NULL);
1240+
((RangeTblRef *) linitial(viewQuery->jointree->fromlist))->rtindex = 1;
1241+
1242+
/* Clear aggregation/grouping/sorting state — all materialized. */
1243+
viewQuery->hasAggs = false;
1244+
viewQuery->groupClause = NIL;
1245+
viewQuery->havingQual = NULL;
1246+
viewQuery->sortClause = NIL;
1247+
viewQuery->distinctClause = NIL;
1248+
viewQuery->hasDistinctOn = false;
1249+
viewQuery->hasWindowFuncs = false;
1250+
viewQuery->hasTargetSRFs = false;
1251+
viewQuery->limitCount = parse->limitCount;
1252+
viewQuery->limitOffset = parse->limitOffset;
1253+
viewQuery->limitOption = parse->limitOption;
1254+
1255+
/* Create subroot for planning the MV scan. */
1256+
subroot = (PlannerInfo *) palloc(sizeof(PlannerInfo));
1257+
memcpy(subroot, root, sizeof(PlannerInfo));
1258+
subroot->parent_root = root;
1259+
subroot->eq_classes = NIL;
1260+
subroot->plan_params = NIL;
1261+
subroot->outer_params = NULL;
1262+
subroot->init_plans = NIL;
1263+
subroot->agginfos = NIL;
1264+
subroot->aggtransinfos = NIL;
1265+
subroot->parse = viewQuery;
1266+
subroot->tuple_fraction = root->tuple_fraction;
1267+
subroot->limit_tuples = root->limit_tuples;
1268+
subroot->append_rel_list = NIL;
1269+
subroot->hasHavingQual = false;
1270+
subroot->hasNonPartialAggs = false;
1271+
subroot->hasNonSerialAggs = false;
1272+
subroot->numOrderedAggs = 0;
1273+
subroot->hasNonCombine = false;
1274+
subroot->numPureOrderedAggs = 0;
1275+
1276+
subroot->processed_tlist = NIL;
1277+
preprocess_targetlist(subroot);
1278+
1279+
/* Compute final locus for the MV scan. */
1280+
{
1281+
PathTarget *newtarget = make_pathtarget_from_tlist(subroot->processed_tlist);
1282+
subroot->final_locus = cdbllize_get_final_locus(subroot, newtarget);
1283+
}
1284+
1285+
/*
1286+
* Plan the MV scan.
1287+
*
1288+
* We need a clean qp_extra with no groupClause or activeWindows,
1289+
* because the rewritten viewQuery is a simple SELECT from the MV
1290+
* with no GROUP BY, windowing, etc. The standard_qp_callback uses
1291+
* qp_extra->groupClause to compute group_pathkeys, which would fail
1292+
* if it still contained the original query's GROUP BY expressions.
1293+
*
1294+
* standard_qp_extra is { List *activeWindows; List *groupClause; },
1295+
* so a zeroed struct of that size works correctly (both fields NIL).
1296+
*/
1297+
{
1298+
char clean_qp_extra[2 * sizeof(List *)];
1299+
memset(clean_qp_extra, 0, sizeof(clean_qp_extra));
1300+
mv_final_rel = query_planner(subroot, qp_callback, clean_qp_extra);
1301+
}
1302+
1303+
/* Cost-based decision: use MV only if cheaper. */
1304+
if (mv_final_rel->cheapest_total_path->total_cost < current_rel->cheapest_total_path->total_cost)
1305+
{
1306+
root->parse = viewQuery;
1307+
root->processed_tlist = subroot->processed_tlist;
1308+
root->agginfos = subroot->agginfos;
1309+
root->aggtransinfos = subroot->aggtransinfos;
1310+
root->simple_rte_array = subroot->simple_rte_array;
1311+
root->simple_rel_array = subroot->simple_rel_array;
1312+
root->simple_rel_array_size = subroot->simple_rel_array_size;
1313+
root->hasNonPartialAggs = subroot->hasNonPartialAggs;
1314+
root->hasNonSerialAggs = subroot->hasNonSerialAggs;
1315+
root->numOrderedAggs = subroot->numOrderedAggs;
1316+
root->hasNonCombine = subroot->hasNonCombine;
1317+
root->numPureOrderedAggs = subroot->numPureOrderedAggs;
1318+
root->hasHavingQual = subroot->hasHavingQual;
1319+
root->group_pathkeys = subroot->group_pathkeys;
1320+
root->sort_pathkeys = subroot->sort_pathkeys;
1321+
root->query_pathkeys = subroot->query_pathkeys;
1322+
root->distinct_pathkeys = subroot->distinct_pathkeys;
1323+
root->eq_classes = subroot->eq_classes;
1324+
root->append_rel_list = subroot->append_rel_list;
1325+
current_rel = mv_final_rel;
1326+
table_close(matviewRel, NoLock);
1327+
need_close = false;
1328+
break;
1329+
}
1330+
else
1331+
{
1332+
/* MV is not cheaper, reset and try next. */
1333+
mv_final_rel = current_rel;
1334+
}
1335+
}
1336+
1337+
if (need_close)
1338+
table_close(matviewRel, AccessShareLock);
1339+
systable_endscan(mvscan);
1340+
table_close(mvauxDesc, AccessShareLock);
1341+
1342+
return current_rel;
1343+
}

src/backend/optimizer/plan/planner.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,17 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
952952
root->partColsUpdated = false;
953953
root->is_correlated_subplan = false;
954954

955+
/*
956+
* Save a copy of the raw parse tree for AQUMV join exact-match.
957+
* This must be done before any preprocessing modifies the parse tree.
958+
*/
959+
if (Gp_role == GP_ROLE_DISPATCH &&
960+
enable_answer_query_using_materialized_views &&
961+
parent_root == NULL)
962+
root->aqumv_raw_parse = copyObject(parse);
963+
else
964+
root->aqumv_raw_parse = NULL;
965+
955966
/*
956967
* If there is a WITH list, process each WITH query and either convert it
957968
* to RTE_SUBQUERY RTE(s) or build an initplan SubPlan structure for it.
@@ -1935,6 +1946,13 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
19351946

19361947
/* Do the real work. */
19371948
current_rel = answer_query_using_materialized_views(root, aqumv_context);
1949+
1950+
/* Try join AQUMV if single-table didn't rewrite. */
1951+
if (current_rel == aqumv_context->current_rel)
1952+
{
1953+
current_rel = answer_query_using_materialized_views_for_join(root, aqumv_context);
1954+
}
1955+
19381956
/* parse tree may be rewriten. */
19391957
parse = root->parse;
19401958
}

src/include/nodes/pathnodes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,8 @@ struct PlannerInfo
505505
int numPureOrderedAggs; /* CDB: number that use ORDER BY/WITHIN GROUP, not counting DISTINCT */
506506
bool hasNonCombine; /* CDB: any agg func w/o a combine func? */
507507
bool is_from_orca; /* true if this PlannerInfo was created from Orca*/
508+
509+
Query *aqumv_raw_parse; /* Raw parse tree for AQUMV join exact-match */
508510
};
509511

510512
/*

src/include/optimizer/aqumv.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ typedef struct AqumvContextData {
4444
typedef AqumvContextData *AqumvContext;
4545

4646
extern RelOptInfo* answer_query_using_materialized_views(PlannerInfo *root, AqumvContextData *aqumv_context);
47+
extern RelOptInfo* answer_query_using_materialized_views_for_join(PlannerInfo *root, AqumvContextData *aqumv_context);
4748

4849
#endif /* AQUMV_H */

0 commit comments

Comments
 (0)